Who Goes There?
Upgrade Your Site's Authentication with the New ASP.NET 2.0 Membership API
Dino Esposito and Andrea Saltarello
Parts of this article are based on a prerelease version of ASP.NET 2.0. Those sections are subject to change.
This article discusses:
|This article uses the following technologies:
ASP.NET, Visual Basic
Code download available at:Membership.exe(148 KB)
The Seeds of the Membership API
Customizing Authentication with Membership
Using the Membership API
Writing a Custom Provider
The Adapter Pattern
In its first release, ASP.NET introduced forms authentication. This provided a powerful framework for handling user authentication in a secure fashion and with an easy-to-use API. The core of forms authentication hasn't changed in ASP.NET 2.0, so most of the tricks and techniques remain usable. However, a lot of work has been done in ASP.NET 2.0 to expose a new membership system that makes it even easier to create more powerful systems for user authentication and management. When you explore the basics of the provider model (upon which this new membership system is based) and its tight integration with forms authentication, you'll inevitably wonder: how does this affect my existing code and database of users and roles?
At first glance, the totally automatic authentication mechanism of ASP.NET 2.0 appears to work only if you rewrite any existing ASP.NET 1.x validation code you may rely on, including password and role management code. Furthermore, it looks like ASP.NET 2.0 requires you to change your user database to comply with the predefined SQL Server™ or Active Directory® scheme. However, these appearances are deceiving.
The keys to ASP.NET 2.0 authentication are the new Membership API (a generic, public API for authenticating users) and the provider model (the underlying specification that makes it possible). In ASP.NET 2.0, authentication is built atop a provider-based, extensible model. This approach lets you easily change the underlying validation code and user management without having to modify the code that interacts with the membership system. This means you can easily adapt your existing code to connect to the authentication machinery of ASP.NET 2.0.
We'll cover the plumbing of the Membership API and its inherently extensible nature, based on pluggable providers. We'll take an existing ASP.NET 1.x authentication mechanism and port it to ASP.NET 2.0, exposing the legacy authentication mechanism through the new Membership API. The code you must write is minimal, and the level of reusability higher than you may expect.
In ASP.NET 1.x, implementing forms authentication is a multi-step procedure. First, tweak the <authentication> section of the app's Web.config file so it will register for forms-based authentication. Then write the login page that will be displayed to requesting users. Finally, you must plan an authentication strategy, set up any necessary storage, and implement any required code. After you place the following in the Web.config file, any page request that lacks a valid authentication ticket will be redirected to login.aspx:
<authentication mode="Forms"> <forms loginUrl="login.aspx" protection="All" /> </authentication> <authorization> <deny users="?"/> </authorization>
A special HTTP module, named FormsAuthenticationModule, is in charge of this check. This module hooks up to two pipeline events: AuthenticateRequest and EndRequest. When the request is intercepted and ready for authentication, the module looks for an authentication ticket that contains the user's credentials. The authentication ticket is an encrypted HTTP cookie with a configurable name (.ASPXAUTH by default). If no valid ticket is found, the request is redirected to the specified login page, whose URL has been read from the configuration file. (For information about implementing this process without the cookie, see the sidebar "Cookieless Authentication".)
The login page is a Web Form that contains, at a minimum, two textboxes and a Submit button. There's no predefined template or mandatory layout for the login page; each app can provide the login page that best suits its needs. One essential element, however, is the code bound to the postback event—typically, the Click event of a Button or LinkButton. This gathers user credentials (as well as other available data, such as a selected Remember Me checkbox) and validates them against the database of users.
Figure 1 shows the typical ASP.NET 1.x code. At the heart is the AuthenticateUser custom method. It receives the user name and password and runs a query against the database of users to determine whether the name and password represent a valid account.
Figure 1 ASP.NET 1.x Login Page
Public Class LoginForm Inherits System.Web.UI.Page Protected errorMsg As System.Web.UI.WebControls.Label Protected userName As System.Web.UI.WebControls.TextBox Protected passWord As System.Web.UI.WebControls.TextBox Protected Button1 As System.Web.UI.WebControls.Button Protected Form1 As System.Web.UI.HtmlControls.HtmlForm Private Sub Button1_Click( _ ByVal sender As Object, ByVal e As EventArgs) Dim user As String = userName.Text Dim password As String = passWord.Text ' Custom authentication If AuthenticateUser(user, password) Then FormsAuthentication.RedirectFromLoginPage(user, false) Else errorMsg.Text = "Sorry, that's not it." End If End Sub Private Function AuthenticateUser( _ ByVal username As String, ByVal password As String) As Boolean Dim connString As String = GetConnectionString() Dim cmdText As String = _ "SELECT COUNT(*) FROM users WHERE " + _ "CAST(RTRIM(username) AS VarBinary)=" + + "CAST(RTRIM(@theUser) AS VarBinary) " + _ "AND CAST(RTRIM(password) AS VarBinary)=" + _ "CAST(RTRIM(@thePassword) AS VarBinary)" Dim conn As SqlConnection = New SqlConnection(connString) Dim cmd As SqlCommand = New SqlCommand(cmdText, conn) cmd.Parameters.Add("@theUser", _ SqlDbType.NVarChar).Value = username cmd.Parameters.Add("@thePassword", _ SqlDbType.NVarChar).Value = password conn.Open() Try Dim found As Integer = CType(cmd.ExecuteScalar(), Integer) Finally conn.Close() End Try Return (found > 0) End Function Private Function GetConnectionString() As String Return "SERVER=localhost;DATABASE=myUserDatabase;" + _ "Integrated Security=SSPI;" End Function End Class
This sort of code is fairly common and, most importantly, it works well. So what's the problem? If you're engaged in the upgrade of an existing application, you can either ignore the new and timesaving features of ASP.NET 2.0 or drop most of the existing code and rewrite it to address a different database (typically, SQL Server) and schema. But in a reasonably complex application, you will have to deal with much more than just some simple code that verifies credentials. Instead, you'll likely have a user manager object, e-mail and role management, a question/answer protocol for password reminders and resets, password policies, and so forth.
The importance of adhering to the ASP.NET 2.0 programming style is not a matter of functionality. Whether you choose to recompile old-but-still-working code or embrace new-and-hot ASP.NET 2.0 components, the actual behavior of the system, the purpose of which is to authenticate users, will not change. However, by going with the ASP.NET 2.0 model, you will be able to take advantage of new controls and a new, made-to-measure API that spans from membership to role and password management. A bunch of security-related controls (see the sidebar, "Security Controls in ASP.NET 2.0") make building login/logout features a snap. You get ready-made GUI controls that let users log in and out, display login status, change passwords and get reminders, and create new user accounts. These are not just occasional features, but common ones that almost all Web apps require.
Figure 2** ASP.NET 2.0 Configuration Tool **
Most applications also need a back-end system to let administrators create and manage users, passwords, and roles. ASP.NET 2.0 provides new controls for UI-related tasks, an IDE configuration tool (see Figure 2), and a common, extensible API to invoke membership and role management functions.
The Seeds of the Membership API
To understand the real added value of a common, extensible API for common, customizable functions, let's take a deeper look at a security-related feature in ASP.NET 1.x that has recently been the target of criticism: storing passwords in the Web.config file. It's doubtful that any enterprise developers would ever seriously consider storing user information as follows (or, worse yet, in the Web.config file but with passwords as clear text):
<authentication mode="Forms"> <forms loginUrl="login.aspx" protection="All"> <credentials passwordFormat="SHA1"> <user name="Joe" password="07B7F3EE06F278D" /> <user name="Bob" password="B966BE960E7CBBD" /> </credentials> </forms> </authentication>
So you may be wondering why ASP.NET would even allow storing user credentials in a configuration file. Well, in doing so, you give the ASP.NET authentication machinery a clear and fixed endpoint where it can query for user data. If you have credentials stored in Web.config, the following code executed within a login page would work quite well:
If FormsAuthentication.Authenticate(user, password) Then FormsAuthentication.RedirectFromLoginPage(user, false) Else errorMsg.Text = "Sorry, that's not it." End If
This code differs from Figure 1 in only one small, yet essential, detail. The task of authenticating users is delegated to a method named Authenticate, which is statically defined in the FormsAuthentication class available from the System.Web assembly.
There are generally two ways to look at this feature. You can think of the Authenticate method as a built-in (and generally not very useful) shortcut to check user names stored in an XML file. Or, a more thoughtful impression is to consider the Authenticate method as the public and immutable interface of a system feature. The availability of an immutable interface makes it possible to create visual components that interact with the page and other controls in a totally automated, codeless, and declarative way.
How would you design a login control from an ASP.NET 1.x perspective? How would you handle the authentication? It seems the simplest trick is to bind the Click event of the Submit button to a user-defined event. The page author handles the event, verifies the passed credentials, and returns true or false. This model works, but requires tight coupling between the page and login form. It also requires some code to be written. Since you're relying on a public and immutable interface to trigger the authentication process, you can embed most of the aforementioned code in the login component. Resulting pages using the login component will be zero-code pages. Of course, it is possible to extend this pattern far beyond the boundaries of the login function.
Customizing Authentication with Membership
A public and fixed API typically means a known and fixed behavior—a strategy and an underlying algorithm. Authentication, for example, means checking credentials. But every application has its own way to do that—through different databases, different schemas, different stored procedures, and so on. As you can see, the same strategy can be implemented through different algorithms.
Figure 3** 1.x Scheme **
In ASP.NET 1.x, the Authenticate method represents a public and fixed API to apply a known strategy, but its implementation is not smart enough to support interchangeable components that customize the way authentication (the actual algorithm) occurs. In ASP.NET 2.0, the Membership API accomplishes a number of tasks that relate to user authentication and management. Unlike the Authenticate method used in ASP.NET 1.x, the Membership API supports the ASP.NET 2.0 provider model, allowing you to specify your own implementation of a given feature. Figure 3 shows the membership scheme for ASP.NET 1.x; Figure 4 shows the same scheme in ASP.NET 2.0. By writing and registering a custom component (the membership provider) you instruct the public Membership API to use your customized methods (as opposed to the default methods) to perform authentication tasks. This model lets you unplug the default implementation of a given feature (user authentication, in this case) and plug in your own.
Figure 4** 2.0 Scheme **
If you're a software purist, or if you simply like formal terms, these concepts will probably be quite familiar. This is just an instance of the strategy pattern. A strategy pattern indicates an expected behavior (for example, sorting) that can be implemented through a variety of interchangeable algorithms (for example, quick sort or merge sort). Each application then selects the algorithm that fits best. In regard to ASP.NET Membership, the membership provider will implement the expected behavior based on a custom algorithm—Active Directory, a SQL Server database, or an Oracle database, for example.
By registering your membership provider of choice in the Web.config file, you instruct your application to create an instance of an object that it may not actually know at compile time. This model is referred to as the factory pattern.
Using the Membership API
The new Membership class in ASP.NET 2.0 reduces the amount of code you have to write for authenticating users, and it supplies a built-in infrastructure for managing roles. Using the features of the Membership subsystem, you can rewrite the code that authenticates a user, as shown in the following code:
Sub Logon_Click(sender As Object, e As EventArgs) Dim user As String = userName.Text Dim password As String = passWord.Text If (Membership.ValidateUser(user, password)) FormsAuthentication.RedirectFromLoginPage(user, false) Else errorMsg.Text = "Sorry, that's not it." End If End Sub
This code is almost identical to that which you would write for an ASP.NET 1.x application, the difference being that this code uses the Membership.ValidateUser static method instead of the FormsAuthentication.Authenticate static method. ValidateUser's actual behavior is determined by the data provider that is registered for use. All providers are required to expose a standard interface, which is rich enough to incorporate several membership-related tasks. ValidateUser provides a wrapper for these APIs, exposing a single method that can be used by your application regardless of which provider is being used. (The provider that should be used is registered in the Web.config file of the application.) You can change the provider, and subsequently change the authentication engine, without having to recompile the application and without having to change any of the code in the page that makes use of the Membership API.
Using the membership capabilities of ASP.NET 2.0 doesn't require a deep understanding of data storage tools and mechanisms (such as SQL Server, stored procedures, and encryption). While still providing myriad configuration options, the Membership API shields you from the various details of how the credentials are stored and verified. The Membership class contains a few static properties and methods that you use in order to obtain a unique identity for each connected user. This user information can also be used with other ASP.NET services, including role-based functions and personalization features.
Figure 5 describes the various properties available for the Membership class. They are all static properties with fairly simple implementations; each property simply accesses the corresponding member on the current provider.Security Controls in ASP.NET 2.0
ASP.NET 2.0 offers several server controls that make it easier to program security-related aspects of a Web application. These are composite controls that provide a rich, customizable user interface and encapsulate a large part of the boilerplate code and markup you would otherwise have to write repeatedly.
Login control Aside from the graphics and page layout, all login pages look very similar. They contain at least two textboxes (for user name and password), a button to validate credentials, perhaps a "Remember Me" checkbox, and possibly links to click if the user has forgotten his password or needs to create a new account. The Login control provides all of this functionality, including the ability to validate the user against the default membership provider. The appearance of the control is fully customizable through templates and style settings. All UI text messages are also customizable through properties of the class.
LoginName control This control captures the name of the currently logged-in user from the User intrinsic object and outputs it using the current style. Internally, the control builds a dynamic instance of a Label control, sets fonts and color accordingly, and displays the text returned by User.Identity.Name. LoginName has a slim programming interface that consists of one property, FormatString, which defines the format of the text to display.
LoginStatus control This control indicates the state of authentication for the current user. The control's UI consists of a link button to log in or log out, depending on the current user logon state. If the user is acting as an anonymous user—that is, he never logged in—the control displays a link button to invite the user to log in. Otherwise, if the user has already successfully passed through the authentication layer, the control displays the Logout button.
LoginView control The LoginView control lets you aggregate the LoginStatus and LoginName controls allowing you to display a custom UI based on the authentication state and role of the user. The control, which is based on templates, simplifies creation of different UIs that are specific to different states (anonymous or authenticated) and roles.
ChangePassword control This provides an out-of-the-box, virtually codeless solution that enables end users to change their password. The control supplies a customizable user interface and built-in behaviors to retrieve an old password and save a new one. The underlying API for password management is the API supplied by the selected membership provider. The ChangePassword control will work in scenarios where a user may or may not be already authenticated. The control detects if a user is authenticated and automatically populates a user name textbox with the name. Even though the user is authenticated, she will still be required to reenter the current password. Once the password has been successfully changed, the control, if properly configured, may send a confirmation e-mail to the user.
PasswordRecovery control This control represents the form that enables a user to recover or reset a lost password and receive it via e-mail. After you drop the PasswordRecovery control onto a Web Form, you may have to make some changes to the membership environment before it will work, but only if you want password recovery to use GetPassword in order to send the password back to the user. PasswordRecovery will automatically change its behavior if it is configured as enablePasswordReset=true and enablePasswordRetrieval=false (the default out of the box). In this case, assuming requiresQuestionAndAnswer is set to true, the control will challenge the user with a question and answer. If the answer matches the one that was previously stored, the control will call ResetPassword. At this point, the control can either display the newly reset password or e-mail the password (based on a configurable e-mail template) to the user. Note that if the stored password is hashed, retrieving it is completely impossible. In this case, the password can only be reset.
CreateUserWizard control This is designed to provide native functionality for creating a new user using the standard Membership API. The control offers a basic behavior that the developer can extend to send a confirmation e-mail to the new user and add steps to the wizard to collect additional information, such as address, phone number, and roles.
Figure 5 Membership Class Properties
|ApplicationName||Gets and sets an optional string to identify the application.|
|EnablePasswordReset||Returns true if the provider supports password reset.|
|EnablePasswordRetrieval||Returns true if the provider supports password retrieval.|
|HashAlgorithmType||Returns a string representing the hash algorithm that will be used for passwords, assuming that the feature has been configured to use hashing (introduced post-Beta 2).|
|MaxInvalidPasswordAttempts||Returns the maximum number of invalid password attempts allowed before the user is locked out.|
|MinRequiredNonAlphanumericCharacters||Returns the minimum number of punctuation characters required in the password.|
|MinRequiredPasswordLength||Returns the minimum required length for a password.|
|PasswordAttemptWindow||Returns the time window for failed password attempts. If the maximum number of invalid password or password answer attempts is exceeded within this time interval (in minutes), the user is locked out.|
|PasswordStrengthRegularExpression||Returns the regular expression with which the password must comply.|
|Provider||Returns an instance of the currently configured provider.|
|Providers||Returns the collection of all registered providers.|
|RequiresQuestionAndAnswer||Returns true if the provider requires a password question/answer when retrieving or resetting the password.|
|UserIsOnlineTimeWindow||Specifies the time window, in minutes, for which the user is considered to be online after user activity.|
As its name suggests, the Initialize method ensures that the Membership class is properly configured and aligned with the settings in the Web.config file. The Provider property returns a reference to the membership provider currently in use. The Initialize method and the Provider property are then used internally in many of the public static properties. For example, when you access Membership.PasswordAttemptWindow, Initialize is first called to ensure configuration is complete and then the PasswordAttemptWindow property is delegated onto the current provider:
Public Shared ReadOnly Property PasswordAttemptWindow As Integer Get Membership.Initialize() Return Membership.Provider.PasswordAttemptWindow End Get End Property
As of Beta 2, ASP.NET 2.0 ships with a predefined provider that targets Active Directory (System.Web.Security.ActiveDirectoryMembershipProvider), and another for SQL Server databases and MDF files in SQL Server Express (System.Web.Security.SqlMembershipProvider). The providers for Microsoft® Access, which were available in Beta 1, are no longer included with ASP.NET 2.0, but could return as sample source code in the future. You can obtain the list of registered providers through the Providers collection. It is important to note that some properties are provider-specific and may not be supported in certain providers. And, of course, providers can implement more properties than just the ones discussed in this article.
Figure 6 lists the methods of the Membership class, including methods for creating, updating, and deleting users. Some of the methods return or accept a MembershipUser object, which contains information that is stored in the system about the user. If you want to use a custom data store, such as an existing database, you can create your own provider, making your data store accessible to any application using the Membership API. You can also register multiple providers in an application, allowing the application to select the appropriate provider at run time.
Figure 6 Membership Class Shared Methods
|CreateUser||Creates a new user and fails if the user already exists.|
|DeleteUser||Deletes the user corresponding to the specified name.|
|FindUsersByEmail||Returns a collection of membership users whose e-mail address matches the specified e-mail address.|
|FindUsersByName||Returns a collection of membership users whose user name matches the specified user name.|
|GeneratePassword||Generates a random password of the specified length. Returns a|
|GetAllUsers||collection of all users.|
|GetNumberOfUsersOnline||Returns the total number of users currently online.|
|GetUser||Retrieves the MembershipUser object associated with the current or specified user.|
|GetUserNameByEmail||Obtains the user name that corresponds to the specified e-mail address. This method assumes that the e-mail address is a unique identifier in the user database.|
|UpdateUser||Takes a MembershipUser object and updates the information stored for the user.|
|ValidateUser||Authenticates a user using supplied credentials.|
You typically use the Web Site Administration Tool (see Figure 2) for offline management of users and passwords. However, to create a new user programmatically, all you need to do is call the CreateUser method. Let's assume you have a login page with two mutually exclusive panels—one for registered users to login and one for new users to register a new account (see Figure 7).
Figure 7** Separate Login Pages **
If you click the link that allows new users to register, the page switches panels and displays the input form for collecting the name and password of the new user. The click handler for the Add button runs the following code:
Sub AddNewUser_Click(sender As Object, e As EventArgs) Membership.CreateUser(NewUserName.Text, NewUserPassword.Text) NewUserPanel.Visible = False LogUserPanel.Visible = True End Sub
Note that the overload of CreateUser used in this example won't actually work with the default membership configuration. While the default configuration enables password-based resets, it requires a question and answer for this operation. As a result, when using the default membership configuration, you'll need to use one of the other overloads.
To delete a user, you use the DeleteUser method, which accepts the registered user name of the user to be deleted:
Retrieving information about a particular user is just as easy, using the GetUser method. This method takes the user name and returns a MembershipUser object:
MembershipUser user = Membership.GetUser("AndreaS")
Once you've got a MembershipUser object, you know all you need to know about a particular user, and you can, for example, programmatically change the password. An application commonly needs to support several operations on passwords, such as changing or resetting it, or sending it to a user (possibly with a question/answer challenge protocol). These functions are all supported by the Membership API, but not necessarily by the underlying provider. If the provider does not support a given feature, an exception is thrown when the method is invoked.
To use the ChangePassword method, you must supply the old password in addition to the new password:
Dim user As MembershipUser = Membership.GetUser("AndreaS") user.ChangePassword(user.GetPassword(), newPassword)
This, however, might not work in some implementations, such as if you configured the membership system to store the passwords in your database hashed and/or salted—a common security measure. In this case, the original password cannot be retrieved, and resetting the password is the only option available if the user has forgotten the current password. To reset the password, you use the ResetPassword method:
Dim user As MembershipUser = Membership.GetUser("AndreaS") string newPassword = user.ResetPassword()
The subsystem of your application that calls ResetPassword is also in charge of sending the new password to the user (for example, via e-mail). Both the GetPassword and ResetPassword methods have a second overload that takes a string parameter. If specified, this string represents the answer to the user's "forgot password" question.
The ability to reset a password, as well as support for a password challenge, is specific to the provider and is configured in the Web.config file. The password challenge question is exposed as a read/write member of the MembershipUser class. When the user is created, the challenge question and answer must be set and stored in the membership database.
Writing a Custom Provider
If you're fine with storing membership data in a SQL Server Express database file and with the standard schema adopted by ASP.NET, there's not much else to do other than using the Web Site Administration Tool to create and fill the database for the current application. But what if you want to use an Oracle database or a SQL Server database with a custom schema? Or, more importantly, what if you invested time and money in building a full membership system for your current ASP.NET 1.x application? Not to worry. You can reuse all or part of this code—just as long as you are willing and able to write a custom membership provider.
All the providers used in ASP.NET 2.0 implement a common set of members; those defined by the ProviderBase abstract class. The class comes with one virtual method (Initialize) and two virtual properties (Name and Description). The Name property returns the official name of the provider class. The Initialize method takes the name of the provider and a name/value collection object packed with the content of the provider's configuration section. A number of abstract classes derive from ProviderBase and act as the base classes for a variety of specific provider tasks, including MembershipProvider, RoleProvider, and a few others.
MembershipProvider is not directly usable, because it consists of several abstract methods. To build a custom membership provider, you inherit from MembershipProvider. Figure 8 provides the list of virtual properties and methods to override.
Figure 8 MembershipProvider Class
Public MustInherit class MembershipProvider Inherits ProviderBase ' Events Public Event ValidatingPassword As _ MembershipValidatePasswordEventHandler ' Methods Public MustOverride Function ChangePassword( _ ByVal username As String, ByVal oldPassword As String, _ ByVal newPassword As String) As Boolean Public MustOverride Function ChangePasswordQuestionAndAnswer( _ ByVal username As String, ByVal password As String, _ ByVal newPasswordQuestion As String, _ ByVal newPasswordAnswer As String) As Boolean Public MustOverride Function CreateUser( _ ByVal username As String, ByVal password As String, _ ByVal email As String, ByVal passwordQuestion As String, _ ByVal passwordAnswer As String, ByVal isApproved As Boolean, _ ByVal providerUserKey As Object, _ <Out> ByRef status As MembershipCreateStatus) As MembershipUser Public MustOverride Function DeleteUser( _ ByVal username As String, _ ByVal deleteAllRelatedData As Boolean) As Boolean Public MustOverride Function FindUsersByEmail( _ ByVal emailToMatch As String, ByVal pageIndex As Integer, _ ByVal pageSize As Integer, <Out> ByRef totalRecords As Integer) _ As MembershipUserCollection Public MustOverride Function FindUsersByName( _ ByVal usernameToMatch As String, ByVal pageIndex As Integer, _ ByVal pageSize As Integer, <Out> ByRef totalRecords As Integer) _ As MembershipUserCollection Public MustOverride Function GetAllUsers( _ ByVal pageIndex As Integer, ByVal pageSize As Integer, _ <Out> ByRef totalRecords As Integer) As MembershipUserCollection Public MustOverride Function GetNumberOfUsersOnline() As Integer Public MustOverride Function GetPassword( _ ByVal username As String, ByVal answer As String) As String Public MustOverride Function GetUser( _ ByVal providerUserKey As Object, ByVal userIsOnline As Boolean) _ As MembershipUser Public MustOverride Function GetUser( _ ByVal username As String, ByVal userIsOnline As Boolean) _ As MembershipUser Public MustOverride Function GetUserNameByEmail( _ ByVal email As String) As String Public MustOverride Function ResetPassword( _ ByVal username As String, ByVal answer As String) As String Public MustOverride Function UnlockUser( _ ByVal userName As String) As Boolean Public MustOverride Sub UpdateUser(ByVal user As MembershipUser) Public MustOverride Function ValidateUser( _ ByVal username As String, ByVal password As String) As Boolean ' Properties Public MustOverride Property ApplicationName As String Public MustOverride ReadOnly Property EnablePasswordReset As Boolean Public MustOverride ReadOnly Property EnablePasswordRetrieval _ As Boolean Public MustOverride ReadOnly Property MaxInvalidPasswordAttempts _ As Integer Public MustOverride ReadOnly Property _ MinRequiredNonAlphanumericCharacters As Integer Public MustOverride ReadOnly Property _ MinRequiredPasswordLength As Integer Public MustOverride ReadOnly Property PasswordAttemptWindow As Integer Public MustOverride ReadOnly Property _ PasswordFormat As MembershipPasswordFormat Public MustOverride ReadOnly Property _ PasswordStrengthRegularExpression As String Public MustOverride ReadOnly Property _ RequiresQuestionAndAnswer As Boolean Public MustOverride ReadOnly Property RequiresUniqueEmail As Boolean End Class
Figure 9 shows an extremely simple, but usable sample class that incorporates the authentication code used in Figure 1 for an ASP.NET 1.x application. As you can see, most methods and properties have a null implementation that just raises an exception. The implementation for ValidateUser, however, calls back into some code imported from the ASP.NET 1.x application. The more features you have, the more you can port to ASP.NET 2.0 by overriding other methods and properties in a more meaningful way. For example, if your existing code includes a user manager object to perform Create/Read/Update/Delete (CRUD) operations on users, you can easily map those members to the methods of the custom membership provider. The companion code for this article, which can be downloaded from the MSDN®Magazine Web site, demonstrates how to port such functionality to ASP.NET 2.0.
Figure 9 Simple Custom Membership Provider
Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Web Imports System.Web.UI Imports System.Web.Security Namespace MsdnMag Public Class MyMembershipProvider : Inherits MembershipProvider Public Sub New() End Sub Private _applicationName As String = "SampleApp" ... ' Members ommitted that simply throw a NotsupportedException Public Overrides Function ValidateUser(ByVal username As String, _ ByVal password As String) As Boolean Return AuthenticateUser(username, password) End Function Public Overrides Property ApplicationName() As String Get Return _applicationName End Get Set(ByVal value As String) _applicationName = value End Set End Property Public Overrides ReadOnly Property EnablePasswordReset() _ As Boolean Get Return False End Get End Property Public Overrides ReadOnly Property EnablePasswordRetrieval() _ As Boolean Get Return False End Get End Property Public Overrides ReadOnly Property _ MinRequiredNonAlphanumericCharacters() As Integer Get Return 1 End Get End Property Public Overrides ReadOnly Property MinRequiredPasswordLength() _ As Integer Get Return 12 End Get End Property Private Function AuthenticateUser( _ ByVal username As String, ByVal password As String) As Boolean ... ' code from AuthenticateUser in Figure 1 End Function End Class End Namespace
To register the new membership provider, you need to tweak the Web.config file as shown here:
<membership defaultProvider="MyMembershipProvider"> <providers> <add name="MyMembershipProvider" type="MsdnMag.MyMembershipProvider" /> </providers> </membership>
Simply add this code to your application's Web.config file under the <system.web> node and you're done. From now on, any call to the Membership class will be resolved calling into the methods of your provider class. Figure 10 shows the Web Site Administration Tool being used to select the new provider.
Figure 10** Managing the New Membership Provider **
The Adapter Pattern
You've now seen that ASP.NET 1.x membership code can be properly retooled and reused in ASP.NET 2.0 applications. No matter how complex your existing authentication engine might be, there is always a way to reuse large portions and integrate these pieces with the cool, new ASP.NET 2.0 security and authentication infrastructure. Essential to achieving these results is another popular design pattern: the adapter pattern.
The adapter pattern converts the programming interface of some class A into an interface B that some client class C fully understands. In other words, the adapter pattern designates a class that wraps the implementation of another class and exposes this second class via a different interface. In this context, the most exciting characteristic of the adapter pattern is that it provides a way for the membership provider class to permit access to its internal implementation (based on existing ASP.NET 1.x code) in such a way that a caller (such as ASP.NET 2.0 login pages or security controls) is not bound to the specifications of that implementation.
On a side note, the adapter pattern can also be used to reuse some functionality out of a class without resorting to inheritance. To paraphrase Erich Gamma and his co-authors in Design Patterns (Addison-Wesley, 1995), you should always favor object composition over inheritance. Inheritance should be primarily used as a way to specialize existing types to take advantage of polymorphism as opposed to a strategy for reusing existing code. Moreover, in a single-inheritance environment like the .NET Framework, this pattern gains importance as it allows you to save the base class for other situations.
Applying the adapter pattern will be easier if you better organize your ASP.NET 1.x code today. Wrapping all of your logic and data into a bunch of classes can only help. At a minimum, you can have a User class that represents the user of your system with all the expected properties and any assigned behaviors. In addition, you can have a collection of User objects and a manager class to perform CRUD tasks. Depending on the desired level of sophistication, you can also have Password and Role objects with their own APIs. Rewriting ASP.NET 1.x code as classes is not mandatory, but it will make it significantly easier to port the code to ASP.NET 2.0.
The ASP.NET 2.0 Membership API provides an extensible model for building your own validation layer to handle user credentials. This is a great benefit because you're not limited to using a particular data store and schema. In addition, there is an array of new server controls available to simplify the creation of security-related user interfaces (see the sidebar on "Security Controls in ASP.NET 2.0" for more information).Cookieless Authentication
In ASP.NET 1.x, cookies are mandatory if you want to take advantage of the built-in authentication framework (assuming you aren't using Microsoft Mobile Internet Toolkit in ASP.NET 1.1). In ASP.NET 2.0, the core forms authentication API also supports cookieless semantics. More precisely, the entire API has been reworked so that it exposes a nearly identical programming interface, but with support for dual semantics: cookied and cookieless.
In theory, there are several ways you can implement cookieless authentication. For instance, you can append the ticket to the query string or store it as extra path information and retrieve it from the PATH_INFO server variable. But ASP.NET 2.0 takes a different approach, which is consistent with the implementation of cookieless sessions. The authentication ticket is packed into the URL, as shown here:
The provider model of ASP.NET 2.0 (an implementation of the strategy pattern) offers an excellent way to avoid having to re-architect your existing solutions. Applied to membership, the provider model allows ASP.NET pages and security controls to connect to custom components that supply predefined interfaces. By applying the adapter pattern, you can take an existing class, typically the entry point to your ASP.NET 1.x membership system, and adapt it to expose any internals via the ASP.NET 2.0 standard interface. In this way, you can plug your old code and data store into the dazzling new world of ASP.NET 2.0. Best of all, you'll have the new features (such as security controls, standard APIs, and integration with the Web Site Administration Tool) but still be able to reuse most of your old code.
Dino Esposito is a mentor at Solid Quality Learning and the author of Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005) Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch with Dino at firstname.lastname@example.org or join the blog at weblogs.asp.net/despos.