In-Depth

Leverage New ASP.NET Security Controls

Learn how to integrate ASP.NET's built-in security tools with the features of your existing site.

Technology Toolbox: Visual Basic, ASP.NET, XML

Security is an important consideration when building any ASP.NET application, not least because these apps typically interface with the Internet and all of its inherent dangers while managing vital company and customer information.

You have an obligation to your customers and your company to ensure that your apps protect the privacy and integrity of the data that your application manipulates. Any application that connects to the Internet faces the risk that fraudulent users will access the application, especially if that application holds credit card information or other high-value data that souls less scrupulous than you might be able to exploit or sell.

Fortunately, ASP.NET ships with a comprehensive security framework, one that can help you do everything from defining what counts as a valid password, to providing a database that holds authentication information to identify valid users.

The downside to the comprehensiveness of these tools is that few organizations want or need these tools to do everything they can provide. For example, most organizations already have a policy for validating passwords. They probably have a table of usernames and passwords, as well. Companies don't want to implement the ASP.NET features as a comprehensive set, but want to cherry pick parts of the ASP.NET security infrastructure, as needed.

I'll walk you through how to either integrate with, or replace, the ASP.NET components listed in Table 1 with your existing security resources. The difficulty in using these components isn't in finding the functionality you need or even in adding your own functionality. The key problems lie in determining which parts you want to take advantage of, where to modify the parts you need to use, and how to bypass the parts that you don't want. I'll show you the minimum code you'll need to control access to your site's folders, how to customize the default ASP.NET providers to match your company's security policies, and how to replace the ASP.NET security providers to give you complete control of your site's security. This isn't a theoretical issue; I'll demonstrate all of these techniques with typical, real-world scenarios. For instance, while many developers panic at the thought of writing their own provider (assuming that it's roughly equivalent to writing a device driver), creating a custom security provider in ASP.NET 2.0 is a task and a practical solution that can be performed by a typical application programmer in just a few hours (see Figure 1 and Table 1).

The examples in this article are intentionally general. No two apps or organizations will have exactly the same needs, so the emphasis here is on how to pull out the specific functionality you need, not to cover every bit of functionality in these security tools. You can then decide how much customization you want to apply (see Figure 2).

Bypass the Login Controls
As an example of how easy it is to customize ASP.NET security, assume that your organization's security infrastructure already includes a standard login page, and you don't want to use the ASP.NET Login controls. You might only want to support using Allow and Deny tags in the Web.config file to restrict access to site folders. This set of tags lets the user, Peter, into the site's folder but denies access to all other users:

<authorization> 
   <allow users="Peter"/ >
   <deny users="*"/>
</authorization>

The only code that you need to add to your login page to support this Web.config setting is a call to the SetAuthCookie method to create an authentication cookie that contains the user's name:

FormsAuthentication.SetAuthCookie( _
   "Peter", False)

The good news is that cherry-picking and customizing the rest of the ASP.NET security framework is just as easy.

If you want to customize the security setting for your site, you must first explicitly declare a membership provider in your Web.config file. Begin by adding a membership element to your Web.config file. Once you have that in place, add a providers element to the Web.config's membership element to hold the list of providers you intend to use. Next, use the Add tag to define the site's provider to the providers element. Finally, make that provider the default provider for your site by specifying the name of your new provider in the membership element's defaultProvider attribute.

If all you want is to customize the default security provider for your site, you must add the provider in the site's Web.config file explicitly:

<connectionStrings>
   <add name="SecurityConnectionString" _
     connectionString= _
     "...connection string for default database?"
     providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>    <membership defaultProvider="SqlProvider">    <providers>      <add name="SqlProvider"         type="System.Web.Security. _         SqlMembershipProvider"         connectionStringName= _         "SecurityConnectionString" />    </providers> </membership>

So far, you've defined the provider. You customize it by overriding properties on the provider with attributes on the Add tag. A typical customization is to change the properties that specify the password format, so that the provider's rules match company policy. Note that the default provider's settings require seven characters with one character being non-alphanumeric. This example eliminates the need for non-alphanumeric characters by overriding the property that specifies the number of characters required:

<add name="SqlProvider" 
   type="System.Web.Security. _
     SqlMembershipProvider" 
   connectionStringName="SecurityConnectionString" 
     minRequiredNonalphanumericCharacters="0"/&glt;

Other attributes allow you to control how many characters are required in a password, as well as the regular expression you want to test passwords against.

Controlling these settings is critical if you want to avoid writing your own security provider, but want to use your own tables of security information instead. You can use your company's security information by copying your security-related information into the aspnet_Membership and aspNet_Users tables whenever a change is made. One roadblock to implementing this solution is that, by default, passwords are stored in the ASP.NET security tables in an encrypted format. However, you can store passwords as entered by the user if you set the passwordFormat attribute of the Add tag to "Clear."

Create a Custom Security Provider
You must write your own security provider if you want to access your own security tables or implement more complex security rules directly. For example, you'll need to do this if you want to prevent users from recycling passwords within a specific time period.

The process for adding your own provider requires three simple steps. First, add a class to your Web site in the App_Code folder and have that class inherit from System.Web.Security.MembershipProvider. Second, provide code for any methods and properties that you want to use in your application. Finally, add the provider in the Web.config file and make it the default provider.

The initial Class looks like this (before Visual Studio generates skeletons for all the routines that must be inherited):

Imports Microsoft.VisualBasic
Public Class NewMembershipProvider    Inherits System.Web.Security.MembershipProvider
End Class

You might be daunted by the number of properties and methods that your MembershipProvider must implement once Visual Studio generates all the required routine skeletons. Furthermore, these members are marked as MustOverride, so you can't simply call the base method.

There is no reason for panic, however. Most of the properties that you must override merely require you to return a simple value. In this example, the MaxInvalidPasswordAttempts property (which controls how many times a user is allowed to respond to the password question before being locked out) is set to return 1, limiting users to a single try:

Public Overrides ReadOnly Property _
   MaxInvalidPasswordAttempts() As Integer
   Get
     Return 1
   End Get
End Property

Providing code for the methods that require it is only slightly more difficult. Note that you don't need to put code in any method that you don't care to support. For example, there's no need to put functional code in the CreateUser method if you don't intend to create new users from your application code. If you get tired of seeing the various warning messages generated by functions that don't have a Return statement, you can simply return Nothing, False, a zero-length string, or 0, depending on the function's datatype.

Most organizations already have a complete system in place for managing security. This means you probably need to implement only two or three methods to create a minimal MembershipProvider. For this example, assume that you need to implement only those methods required to integrate with the ASP.NET Login control and to support gathering information about a user from code.

The ASP.NET Login control validates users by calling your MembershipProvider's ValidateUser method and passing the method the username and password that the user enters. You must return True from the method if you determine that the username and password are valid (the authorization cookie will be generated for you):

Public Overrides Function ValidateUser(ByVal username _
   As String, ByVal password As String) As Boolean
   If MyCustomValidateMethod(username, _
     password) = True Then  
       Return True
   Else
     Return False
   End If
End Function

Gather Info About the User
Your MembershipProvider must implement the GetUser method to support gathering information about the current user from within your code. ASP.NET passes the name of the currently logged-on user to the method, and your code must return a MembershipUser object that holds information about the user. Creating a MembershipUser object requires passing thirteen parameters, but you need to provide values for only the first two: the user's name and your provider's name. You can retrieve the provider's name from the base object's Name property, so you can implement the GetUser method with only a couple lines of code:

Public Overloads Overrides Function GetUser(ByVal _
   username As String, ByVal userIsOnline _
   As Boolean) As _
   System.Web.Security.MembershipUser
Dim musr As MembershipUser
   musr = New MembershipUser(Me.Name, _      _UserName, Nothing, Nothing, Nothing, _      Nothing, Nothing, False, Nothing, Nothing, _      Nothing, Nothing, Nothing    Return musr End Function

In a real application, you would add code that confirms that the username passed to the GetUser method is a valid user (possibly by checking for the username in a table), then retrieve information from your own security tables to fill out the rest of the parameters.

Typically, you also want to implement the MembershipProvider's Initialize method to retrieve any configuration information that your provider needs. The Initialize method is called automatically when ASP.NET loads your MembershipProvider and is passed two parameters: the name of your provider and a NameValueCollection that lists all the attributes from the Add parameter that defines your provider.

It's not unusual for organizations to keep security information for different applications in different tables in the same database. You can store this configuration information in your application's Web.config file by adding a new tableName attribute to your Add tag to specify which table you want your MembershipProvider to use:

<add name="NewProvider" 
   type="NewMembershipProvider" 
   connectionStringName="SecurityConnectionString" 
   tableName="tblSalesOrder" />

You retrieve a given attribute's value by using the name of the attribute whose value you want to retrieve with the second parameter passed to the Initialize method. This example retrieves the tableName attribute set in the previous example, and then uses it to initialize a module-level variable that other routines in the provider can use. The code also calls the base Initialize method to ensure that any configuration information required by the base object is processed:

Private _TableName As String
Public Overrides Sub Initialize(ByVal name As String, _    ByVal config As System.Collections. _    Specialized.NameValueCollection)
   _TableName = config("tableName")    MyBase.Initialize(name, config)
End Sub

You must rewrite the Web.config file to use your provider as the default provider. Also, you must provide the name of your class file as the type when defining your provider:

<membership defaultProvider="NewProvider" >
   <providers>
     <add  name="NewProvider"
         type="NewMembershipProvider"
        connectionStringName= _
         "SecurityConnectionString"  />
   </providers>
</membership>

Customize MembershipUser
The GetUser method returns a MembershipUser object with properties that give your code access to security-related information, including the name of the current user and whether the user is currently locked out of the site. The Membership object's properties also provide security-related functions, such as the ability to reset the user's password and the ability to remove the ban on a locked-out user. However, you need to create your own MembershipUser object if the MembershipUser object doesn't return the data that you want.

Note that you aren't required to make any entries in the Web.config file to use a custom MembershipUser. Begin by adding another class module to your App_Code folder that inherits from the default MembershipUser object:

Imports Microsoft.VisualBasic
Public Class NewMembershipUser    Inherits MembershipUser
End Class

The MembershipUser object has no required methods or properties that you must implement. However, you should create a constructor that accepts the values that your MembershipUser will return, and provide properties for accessing those values. This code defines a MembershipUser object that accepts a name and authorization code along with a read-only property that returns the authorization code:

Private _Name As String
Private _AuthCode As String
Sub New(ByVal name As String, ByVal authCode _    As String)    _Name = name    _AuthCode = authCode End Sub
Public ReadOnly Property AuthCode() As String    Get    Return _AuthCode    End Get End Property

Next, you need to implement the GetUser method with code that creates and returns the new MembershipUser object. This example uses a hard-coded value for the authCode parameter, but you would pull the value from your own tables:

Public Overloads Overrides Function GetUser(ByVal _
   username As String, ByVal userIsOnline As _
   Boolean) As System.Web.Security.MembershipUser
Dim musr As NewMembershipUser
musr = New NewMembershipUser(username, "SP") Return musr
End Function

Finally, your application code needs to capture the result in a variable defined as your new MembershipUser object, which enables you to access your new properties. This code checks the AuthCode before executing some restricted code:

Dim nuser As NewMembershipUser
   nuser = Membership.GetUser()
     If nuser.AuthCode = "SP" Then
     ?restricted code? 
   End If

Customizing Roles
Most security systems organize their users into groups to simplify managing security settings. In ASP.NET, you refer to these groups as roles, and you manage them through a RoleProvider. As with the MembershipProvider, you can substitute your own RoleProvider that accesses your own security tables.

Substituting your own RoleProvider enables you to support the roles attribute in the authorization element's Allow and Deny tags, based on information in your security tables. For example, this authorization element allows access only to users in the Manager role:

<authorization>
   <allow roles="Manager"/ >
   <deny users="*"/>
</authorization>

You use the Roles element in the Web.config file to enable roles for your application and to specify your own RoleProvider. This example makes a class called NewRoleProvider the provider for the site:

<roleManager enabled="true" defaultProvider="RoleProvider" >
   <providers>
     <add name="RoleProvider"
        type="NewRoleProvider"/ >
   </providers>
</roleManager*>

You often need to support the IsInRole method of the User object. This method must check to see whether a user is in the role passed to the method and return True if the user has that role. This example checks programmatically to see whether the user is in the "Manager" role before letting some restricted code execute:

If Me.Context.User.IsInRole("Manager") = True Then
   ...restricted code...
End If

You need to put code only in your RoleProvider's GetRolesForUser method to support both the roles attribute on the Allow/Deny tags and the IsInRole method.

As you did with the MembershipProvider, you need to create a class file in your site's App_Code folder (this time inheriting from System.Web.Security.RoleProvider). A typical example looks like this (before Visual Studio adds the required members):

Imports Microsoft.VisualBasic
Public Class NewRoleProvider    Inherits System.Web.Security.RoleProvider
End Class

Your GetRolesForUser method must accept a username, either when it is called from ASP.NET to support the roles attribute or from your own code to support the IsInRole method. You must return a string array of the roles that the user belongs to, or return nothing if the user doesn't exist or doesn't belong to any roles. This example builds an array with a single entry if the username is "Peter"; otherwise, the routine returns Nothing:

Public Overrides Function GetRolesForUser(ByVal
username As String) As String()
   If username = "Peter" Then
     Dim roles(0) As String
     roles(0) = "Manager"
     Return role
Els   
     Return Nothing
   End If
End Function

In a real-world scenario, you'd want to use the username to access a table of role information to build your array. And, as with the MembershipProvider, the RoleProvider has an Initialize method that you can use to pass configuration information from the RoleProvider's Add tag in the Web.config file to your code.

The examples in this article cover only the routines necessary for a minimal implementation of the security and role providers. You can use this skeleton to add code to the other methods to create more full-fledged providers. The more methods you support, the more standard ASP.NET controls you can use in your applications. For example, supporting the various methods for changing passwords lets you use the ASP.NET PasswordRecovery and ChangePassword controls. Even with these additional goals, it should only take you a few hours to integrate your security tables and policies with the ASP.NET security framework fully.

comments powered by Disqus

Featured

Subscribe on YouTube