Programming Microsoft ASP.NET 2.0 Core Reference
The Membership and Role Management API
In ASP.NET 2.0, the core of Forms authentication is the same as in ASP.NET 1.x. Most of the tricks and techniques you have learned remain valid and usable. The most notable change to Forms authentication in ASP.NET 2.0 is the introduction of a complementary API the membership API. The membership API provides a set of classes to let you manage users and roles.
Partnered with the FormsAuthentication class, the new Membership and Roles classes form a complete security toolkit for ASP.NET developers. The Membership class supplies methods to manage user accounts for adding or deleting a new user and editing any associated user information such as e-mail address and password. The Roles class creates and manages associations between users and roles.
What does the expression "managing user accounts" mean exactly? Simply put, it states that the Membership class knows how to create a new user or change his or her password. How do you create a user? Typically, you add a new record to some sort of data store. If that's the case, who is in charge of deciding which data store to use and how to actually write the new user information? These tasks represent the core functionality the membership API is designed to provide.
The membership API doesn't bind you to a fixed data store and data scheme. Quite the reverse, I'd say. It leaves you free to choose any data store and scheme you want, but it binds you to a fixed API through which users and roles are managed. The membership API is based on the provider model (discussed in Chapter 1) and delegates to the selected provider the implementation of all the features defined by the API itself.
The Membership Class
Centered around the Membership static class, the membership API shields you from the details of how the credentials and other user information are retrieved and compared. The Membership class contains a few methods that you use to obtain a unique identity for each connected user. This information can also be used with other ASP.NET services, including role-based function enabling and personalization.
Among the members of the class are methods for creating, updating, and deleting users, but not methods for managing roles and programmatically setting what a user can and cannot do. For that you'll have to turn to the Roles class, which we'll cover later.
The Membership class defaults to a provider that stores user information to a SQL Express database in a predefined format. If you want to use a custom data store (such as a personal database), you can create your own provider and just plug it in.
The Programming Interface of the Membership Class
Table 15-8 lists the properties exposed by the Membership class.
Property | Description |
---|---|
ApplicationName | A string to identify the application. Defaults to the application's root path. |
EnablePasswordReset | Returns true if the provider supports password reset. |
EnablePasswordRetrieval | Returns true if the provider supports password retrieval. |
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 number of minutes in which a maximum number of invalid password or password answer attempts are allowed before the user is locked out. |
PasswordStrengthRegularExpression | Returns the regular expression that the password must comply with. |
Provider | Returns an instance of the provider being used. |
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 | Number of minutes after the last activity for which the user is considered online. |
The Provider property returns a reference to the membership provider currently in use. As you'll see in a moment, the provider is selected in the configuration file. ASP.NET 2.0 comes with a couple of predefined providers that target MDF files in SQL Server Express and Active Directory. However, many more membership providers are in the works from Microsoft and third-party vendors. You can obtain the list of available providers through the Providers collection.
All properties are static and read-only. All of them share a pretty simple implementation. Each property just accesses the corresponding member on the current provider, as shown here:
public static int PasswordAttemptWindow { get { Membership.Initialize(); return Membership.Provider.PasswordAttemptWindow; } }
As the name suggests, the Initialize method ensures that the internal structure of the Membership class is properly initialized and that a reference to the provider exists.
The class supports fairly advanced functionality, such as estimating the number of users currently using the application. It uses the value assigned to the UserIsOnlineTimeWindow property to determine this number. A user is considered online if he has done something with the application during the previous time window. The default value for the UserIs-OnlineTimeWindow property is 15 minutes. After 15 minutes of inactivity, a user is considered offline.
Table 15-9 details the methods supported by the Membership class. This list clarifies the tasks the class accomplishes.
Member | Description |
---|---|
CreateUser | Creates a new user and fails if the user already exists. The method returns a MembershipUser object representing any available information about the user. |
DeleteUser | Deletes the user corresponding to the specified name. |
FindUsersByEmail | Returns a collection of MembershipUser objects whose e-mail address corresponds to the specified e-mail. |
FindUsersByName | Returns a collection of MembershipUser objects whose user name matches the specified user name. |
GeneratePassword | Generates a random password of the specified length. |
GetAllUsers | Returns a 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. If more users share the same e-mail, the first is retrieved. |
UpdateUser | Takes a MembershipUser object and updates the information stored for user. |
ValidateUser | Authenticates a user by using supplied credentials. |
Setting Up Membership Support
To build an authentication layer based on the membership API, you start by choosing the default provider and establishing the data store. In the simplest case, you can stay with the default predefined provider, which saves user information in a local MDF file in SQL Server 2005 Express.
The Web Site Administration Tool (WSAT) in Visual Studio .NET 2005 provides a user interface for creating and administering the registered users of your application. Figure 15-9 provides a glimpse of the user interface.
To add a new user or to edit properties of an existing one, you use the links shown in the figure. When you edit the properties of a new user, you use the page shown in Figure 15-10.
Validating Users
At this point, we're ready to write some code that uses the membership API. Let's start with the most common operation authenticating credentials. Using the features of the membership subsystem, you can rewrite the code in the login page you saw previously to authenticate a user as follows:
void LogonUser(object sender, EventArgs e) { string user = userName.Text; string pswd = passWord.Text; if (Membership.ValidateUser(user, pswd)) FormsAuthentication.RedirectFromLoginPage(user, false); else errorMsg.Text = "Sorry, yours seems not to be a valid account."; }
This code doesn't look much different from what you would write for an ASP.NET 1.x application, but there's one big difference: the use of the ValidateUser built-in function. Here is the pseudocode of this method as it is implemented in the system.web assembly:
public static bool ValidateUser(string username, string password) { return Membership.Provider.ValidateUser(username, password); }
As you can see, all the core functionality that performs the authentication lives in the provider. What's nice is that the name of the provider is written in the web.config file and can be changed without touching the source code of the application. The overall schema is illustrated in Figure 15-11.
Managing Users and Passwords
The Membership class provides easy-to-use methods for creating and managing user data. For example, to create a new user programmatically, all you do is place a call to the CreateUser method:
Membership.CreateUser(userName, pswd);
To delete a user, you call the DeleteUser method:
Membership.DeleteUser(userName);
You can just as easily get information about a particular user by using the GetUser method. The method takes the user name and returns a MembershipUser object:
MembershipUser user = Membership.GetUser("DinoE");
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 (or other user-specific information). An application commonly needs to execute several operations on passwords, including changing the password, sending a user her password, or resetting the password, possibly with a question/answer challenge protocol. Here is the code that changes the password for a user:
MembershipUser user = Membership.GetUser("DinoE"); user.ChangePassword(user.GetPassword(), newPswd);
To use the ChangePassword method, you must pass in the old password. In some cases, you might want to allow users to simply reset their passwords instead of changing them. You do this by using the ResetPassword method:
MembershipUser user = Membership.GetUser("DinoE"); string newPswd = user.ResetPassword();
In this case, the page 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 underlying membership provider matches the supplied answer against the stored answers; if a user is identified, the password is reset or returned as appropriate.
Note | It goes without saying that the ability to reset the password, as well as support for the password's question/answer challenge protocol, is specific to the provider. You should note that not all the functions exposed by the membership API are necessarily implemented by the underlying provider. If the provider does not support a given feature, an exception is thrown if the method is invoked. |
The Membership Provider
The beauty of the membership model lies not merely in the extremely compact code you need to write to validate or manage users but also in the fact that the model is abstract and extensible. For example, if you have an existing data store filled with user information, you can integrate it with the Membership API without much effort. All you have to do is write a custom data provider that is, a class that inherits from MembershipProvider, which, in turn, inherits from ProviderBase:
public class MyAppMembershipProvider : MembershipProvider { // Implements all abstract members of the class and, if // needed, defines custom functionality ... }
This approach can be successfully employed to migrate existing ASP.NET 1.x authentication code to ASP.NET 2.0 applications and, perhaps more important, to link a custom and existing data store to the membership API. We'll return to this subject in a moment.
The ProviderBase Class
All the providers used in ASP.NET 2.0 not just membership providers implement a common set of members: the members defined by the ProviderBase class. The class comes with one method, Initialize, and one property, Name. 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. The method is supposed to initialize its internal state with the values just read out of the web.config file.
The MembershipProvider Class
Many of the methods and properties used with the Membership class are actually implemented by calling into a corresponding method or property in the underlying provider. It comes as no surprise, then, that many of the methods listed in Table 15-10, which are defined by the MembershipProvider base class, support the functions you saw in Table 15-9 that are implemented by the dependent Membership class. However, note that although Table 15-9 and Table 15-10 are very similar, they are not identical.
Method | Description |
---|---|
ChangePassword | Takes a user name in addition to the old and new password and changes the user's password. |
ChangePasswordQuestionAndAnswer | Takes a user name and password and changes the pair of question/answer challenges that allows reading and changing the password. |
CreateUser | Creates a new user account, and returns a Membership-User-derived class. The method takes the user name, password, and e-mail address. |
DeleteUser | Deletes the record that corresponds to the specified user name. |
FindUsersByEmail | Returns a collection of membership users whose e-mail address corresponds to the specified e-mail. |
FindUsersByName | Returns a collection of membership users whose user name matches the specified user name. |
GetAllUsers | Returns the collection of all users managed by the provider. |
GetNumberOfUsersOnline | Returns the number of users that are currently considered to be online. |
GetPassword | Takes the user name and the password's answer and returns the current password for the user. |
GetUser | Returns the information available about the specified user name. |
GetUserNameByEmail | Takes an e-mail address and returns the corresponding user name. |
ResetPassword | Takes the user name and the password's answer and resets the user password to an auto-generated password. |
UpdateUser | Updates the information available about the specified user. |
ValidateUser | Validates the specified credentials against the stored list of users. |
All these methods are marked as abstract virtual in the class (must-inherit, overridable in Visual Basic .NET jargon). The MembershipProvider class also features a few properties. They are listed in Table 15-11.
Property | Description |
---|---|
ApplicationName | Returns the provider's nickname |
EnablePasswordReset | Indicates whether the provider supports password reset |
EnablePasswordRetrieval | Indicates whether the provider supports password retrieval |
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 number of minutes in which a maximum number of invalid password attempts are allowed before the user is locked out |
PasswordStrengthRegularExpression | Returns the regular expression that the password must comply with |
RequiresQuestionAndAnswer | Indicates whether the provider requires a question/answer challenge to enable password changes |
RequiresUniqueEmail | Indicates whether the provider is configured to require a unique e-mail address for each user name |
Extending the Provider's Interface
The provider can also store additional information with each user. For example, you can derive a custom class from MembershipUser, add any extra members, and return an instance of that class via the standard GetUser method of the membership API.
To use the new class, you cast the object returned by GetUser to the proper type, as shown here:
MyCompanyUser user = (MyCompanyUser) Membership.GetUser(name);
In addition to the members listed in Table 15-10 and Table 15-11, a custom membership provider can add new methods and properties. These are defined outside the official schema of the provider base class and are therefore only available to applications aware of this custom provider:
MyCompanyProvider prov = (MyCompanyProvider) Membership.Provider;
Note | The Providers collection property allows you to use a dynamically selected provider: MembershipProvider prov = Membership.Providers["ProviderName"]; This feature allows applications to support multiple providers simultaneously. For example, you can design your application to support a legacy database of users through a custom provider, while storing new users in a standard SQL Server 2005 table. In this case, you use different membership providers for different users. |
A Custom Provider for ASP.NET 1.x Code
Earlier in the chapter, we discussed a few sample pages using the Employees table in the SQL Server 2000 Northwind database as the data store for user accounts. Let's turn this into a membership provider and register it with the WSAT tool:
public class MyMembershipProvider : MembershipProvider { public MyMembershipProvider() { } public override bool ChangePassword(string username, string oldPassword, string newPassword) { // If you don't intend to support a given method // just throw an exception throw new NotSupportedException(); } ... public override bool ValidateUser(string username, string password) { return AuthenticateUser(username, password); } private bool AuthenticateUser(string username, string pswd) { // Place here any analogous code you may have in your // ASP.NET 1.x application } }
You define a new class derived from MembershipProvider. In this class definition, you have to override all the members in Table 15-10 and Table 15-11. If you don't intend to support a given method or property, for that method just throw a NotSupportedException exception. For the methods you do plan to support which for the previous example included only Validate-User you write the supporting code. At this point, nothing prevents you from reusing code from your old application. There are two key benefits with this approach: you reuse most of your code (perhaps with a little bit of refactoring), and your application now embraces the new membership model of ASP.NET 2.0.
Generally speaking, when writing providers there are three key issues to look at: lifetime of the provider, thread-safety, and atomicity. The provider is instantiated as soon as it proves necessary but only once per ASP.NET application. This fact gives the provider the status of a stateful component, but at the price of protecting that state from cross-thread access. A provider is not thread-safe, and it will be your responsibility to guarantee that any critical data is locked before use. Finally, some functions in a provider can be made of multiple steps. Developers are responsible for ensuring atomicity of the operations either through database transactions (whenever possible) or through locks. For more information, refer to the ASP.NET 2.0 Provider Toolkit at http://msdn.microsoft.com/asp.net/provider.
Configuring a Membership Provider
You register a new provider through the <membership> section of the web.config file. The section contains a child <providers> element under which additional providers are configured:
<membership> <providers> <add name="MyMembershipProvider" type="ProAspNet20.MyMembershipProvider" /> <providers> </membership>
You can change the default provider through the defaultProvider attribute of the <membership> section. Figure 15-12 shows the new provider in WSAT.
With the new provider in place, the code to verify credentials reduces to the following code, which is the same as you saw earlier in the chapter:
void LogonUser(object sender, EventArgs e) { string user = userName.Text; string pswd = passWord.Text; if (Membership.ValidateUser(user, pswd)) FormsAuthentication.RedirectFromLoginPage(user, false); else errorMsg.Text = "Sorry, yours seems not to be a valid account."; }
There's more than just this with the membership API. Now a login page has a relatively standard structure and a relatively standard code attached. At least in the simplest scenarios, it can be reduced to a composite control with no binding code. This is exactly what security controls do. Before we get to cover this new family of server controls, though, let's review roles and their provider-based management.
Managing Roles
Roles in ASP.NET simplify the implementation of applications that require authorization. A role is just a logical attribute assigned to a user. An ASP.NET role is a plain string that refers to the logical role the user plays in the context of the application. In terms of configuration, each user can be assigned one or more roles. This information is attached to the identity object, and the application code can check it before the execution of critical operations.
For example, an application might define two roles Admin and Guest, each representative of a set of application-specific permissions. Users belonging to the Admin role can perform tasks that other users are prohibited from performing.
Note | Assigning roles to a user account doesn't add any security restrictions by itself. It is the responsibility of the application to ensure that authorized users perform critical operations only if they are members of a certain role. |
In ASP.NET, the role manager feature simply maintains the relationship between users and roles. ASP.NET 1.1 has no built-in support for managing roles. You can attach some role information to an identity, but this involves writing some custom code. Checking roles is easier, but ASP.NET 2.0 makes the whole thing significantly simpler.
Note | The Role Management API, although it consists of different methods and properties, works like the Membership API from a mechanical standpoint. Many of the concepts you read in the previous section also apply to role management. |
The Role Management API
The role management API lets you define roles as well as specify programmatically which users are in which roles. The easiest way to configure role management, define roles, add users to roles, and create access rules is to use WSAT. (See Figure 15-12.) You enable role management by adding the following script to your application's web.config file:
<roleManager enabled="true" />
You can use roles to establish access rules for pages and folders. The following <authorization> block states that only Admin members can access all the pages controlled by the web.config file:
<configuration> <system.web> <authorization> <allow roles="Admin" /> <deny users="*" /> </authorization> </system.web> <configuration>
WSAT provides a visual interface for creating associations between users and roles. If necessary, you can instead perform this task programmatically by calling various role manager methods. The following code snippet demonstrates how to create the Admin and Guest roles and populate them with user names:
Roles.CreateRole("Admin"); Roles.AddUsersToRole("DinoE", "Admin"); Roles.CreateRole("Guest"); string[] guests = new string[2]; guests[0] = "JoeUsers"; guests[1] = "Godzilla"; Roles.AddUsersToRole(guests, "Guest")
At run time, information about the logged-in user is available through the HTTP context User object. The following code demonstrates how to determine whether the current user is in a certain role and subsequently enable specific functions:
if (User.IsInRole("Admin")) { // Enable functions specific to the role ... }
When role management is enabled, ASP.NET 2.0 looks up the roles for the current user and binds that information to the User object. This same feature had to be manually coded in ASP.NET 1.x.
Note | In ASP.NET 1.x, you typically cache role information on a per-user basis through a cookie or for all users in a custom Cache entry. In both cases, you do this when the application starts by handling the Application_Start event in the global.asax file. After that, you write a get function to read role information from the store and call it wherever required. |
The Roles Class
When role management is enabled, ASP.NET creates an instance of the Roles class and adds it to the current request context the HttpContext object. The Roles class features the methods listed in Table 15-12.
Method | Description |
---|---|
AddUsersToRole | Adds an array of users to a role. |
AddUsersToRoles | Adds an array of users to multiple roles. |
AddUserToRole | Adds a user to a role. |
AddUserToRoles | Adds a user to multiple roles. |
CreateRole | Creates a new role. |
DeleteCookie | Deletes the cookie that the role manager used to cache all the role data. |
DeleteRole | Deletes an existing role. |
FindUsersInRole | Retrieves all the user names in the specified role that match the provider user name string. The user names found are returned as a string array. |
GetAllRoles | Returns all the available roles. |
GetRolesForUser | Returns a string array listing the roles that a particular member belongs to. |
GetUsersInRole | Returns a string array listing the users that belong to a particular role. |
IsUserInRole | Determines whether the specified user is in a particular role. |
RemoveUserFromRole | Removes a user from a role. |
RemoveUserFromRoles | Removes a user from multiple roles. |
RemoveUsersFromRole | Removes multiple users from a role. |
RemoveUsersFromRoles | Removes multiple users from multiple roles. |
RoleExists | Returns true if the specified role exists. |
Table 15-13 lists the properties available in the Roles class. All the properties are static and read-only. They owe their value to the settings in the <roleManager> configuration section.
Property | Description |
---|---|
ApplicationName | Returns the provider's nickname. |
CacheRolesInCookie | Returns true if cookie storage for role data is enabled. |
CookieName | Specifies the name of the cookie used by the role manager to store the roles. Defaults to .ASPXROLES. |
CookiePath | Specifies the cookie path. |
CookieProtectionValue | Specifies an option for securing the role's cookie. Possible values are All, Clear, Hashed, and Encrypted. |
CookieRequireSSL | Indicates whether the cookie requires SSL. |
CookieSlidingExpiration | Indicates whether the cookie has a fixed expiration time or a sliding expiration. |
CookieTimeout | Returns the time, in minutes, after which the cookie will expire. |
CreatePersistentCookie | Creates a role cookie that survives the current session. |
Domain | Indicates the domain of the role cookie. |
Enabled | Indicates whether role management is enabled. |
MaxCachedResults | Indicates the maximum number of roles that can be stored in a cookie for a user. |
Provider | Returns the current role provider. |
Providers | Returns a list of all supported role providers. |
Some methods in the Roles class need to query continuously for the roles associated with a given user, so when possible, the roles for a given user are stored in an encrypted cookie. On each request, ASP.NET checks to see whether the cookie is present; if so, it decrypts the role ticket and attaches any role information to the User object. By default, the cookie is a session cookie and expires as soon as the user closes the browser.
Note that the cookie is valid only if the request is for the current user. When you request role information for other users, the information is read from the data store using the configured role provider.
Note | Role management passes through the role manager HTTP module. The module is responsible for adding the appropriate roles to the current identity object, such as the User object. The module listens for the AuthenticateRequest event and does its job. This is exactly the kind of code you need to write in ASP.NET 1.x. |
The Role Provider
For its I/O activity, the role manager uses the provider model and a provider component. The role provider is a class that inherits the RoleProvider class. The schema of a role provider is not much different from that of a membership provider. Table 15-14 details the members of the RoleProvider class.
Method | Description |
---|---|
AddUsersToRoles | Adds an array of users to multiple roles |
CreateRole | Creates a new role |
DeleteRole | Deletes the specified role |
FindUsersInRole | Returns the name of users in a role matching a given user name pattern |
GetAllRoles | Returns the list of all available roles |
GetRolesForUser | Gets all the roles a user belongs to |
GetUsersInRole | Gets all the users who participate in the given role |
IsUserInRole | Indicates whether the user belongs to the role |
RemoveUsersFromRoles | Removes an array of users from multiple roles |
RoleExists | Indicates whether a given role exists |
You can see the similarity between some of these methods and the programming interface of the Roles class. As we've seen for membership, this is not just coincidental.
ASP.NET 2.0 ships with two built-in role providers AspNetSqlRoleProvider (default) and AspNetWindowsTokenRoleProvider. The former stores role information in the same MDF file in SQL Server 2005 Express as the default membership provider. For the latter, role information is obtained based on the settings defined for the Windows domain (or Active Directory) the user is authenticating against. This provider does not allow for adding or removing roles.
Custom role providers can be created deriving from RoleProvider and registered using the child <providers> section in the <roleManager> section. Note that the process for doing so is nearly identical to the process you saw for the custom membership provider we explored previously.