The Membership Provider


The Membership provider is what ASP.NET uses to store and validate user credentials. Through a consistent API and object model, developers can create code that works with Membership providers that will continue to function properly, regardless of the underlying credentials database (SQL, XML, Oracle, Access, and so on). In fact, ASP.NET ships with several visual controls that work with the Membership provider, such as the Login, LoginStatus, and LoginView controls. This section shows you the functionality provided by the MembershipProvider abstract base class, discusses the creation of a Membership schema, and then shows you the code and installation instructions for a sample custom Membership Provider.

Introduction to the MembershipProvider Base Class

To create your own custom Membership Provider, you need to create a class that inherits from MembershipProvider. Before doing that, you should know what methods and properties are exposed by this class, their purpose, and what functionality you should include in your derivative class. Tables 29.1 and 29.2 illustrate the properties and methods you will be overriding in your custom implementation.

Table 29.1. MembershipProvider Properties

Property

Description

ApplicationName

The name of the application. Used to differentiate between users with the same name but in two different applications.

EnablePasswordReset

Indicates whether users can request to have their passwords reset.

EnablePasswordRetrieval

Indicates whether users can have their passwords sent to them upon request.

MaxInvalidPassword-Attempts

The number of times a user can fail a validation attempt before being locked out.

MinRequiredNonAlpha-NumericCharacters

The minimum number of non-alphanumeric characters required for a strong password.

MinRequiredPasswordLength

The minimum number of characters required for a strong password.

PasswordAttemptWindow

The time period, in minutes, that can elapse between times when a user exceeds the MaxInvalidPasswordAttempts value.

PasswordFormat

The format of passwords: Clear, Hashed, or Encrypted.

PasswordStrengthRegularExpression

The regular expression used for validating new passwords.

RequiresQuestionAndAnswer

Indicates whether or not the user must supply the answer to their secret question to change, reset, or request their password.

RequiresUniqueEmail

Indicates whether the user data store can contain multiple users with the same email address.


Table 29.2. MembershipProvider Methods

Method

Description

ChangePassword

Changes the user's password.

ChangePasswordQuestion-AndAnswer

Changes the user's password as well as the question/answer pair.

CreateUser

Creates a new user.

DeleteUser

Deletes an existing user.

FindUsersByEmail

Returns a list of users whose email address matches the one supplied.

FindUsersByName

Returns a list of users whose user name matches the supplied name.

GetAllUsers

Returns all of the users in the underlying data store.

GetNumberOfUsersOnline

Returns the number of users in the data store who have been active within the configured timeout period.

GetPassword

Returns a given user's password. This is the data store format, so it may be hashed or encrypted.

GetUser

Returns a MembershipUser object based on a supplied unique identifier.

GetUserNameByEmail

Returns the name of the user whose email address matches the one supplied.

ResetPassword

Resets the user's password.

UnlockUser

If a user has been locked out, this method will clear that status.

UpdateUser

Updates changes made to a given user to the underlying data store.

ValidateUser

Validates the supplied credentials.


Implementing a Membership Schema

When working with your own custom Membership Provider, the main thing to keep in mind is that the Membership provider works almost exclusively with the following classes: MembershipUser and MembershipUserCollection. The MembershipUserCollection class is just a simple collection class containing MembershipUser instances. The MembershipUser class will provide you with your biggest clue as to what information your MemberShip schema needs to define. This class is a fairly straightforward placeholder with a few methods and the following properties:

  • Comment Application-specific (string) data related to the user.

  • CreationDate The date the user was created.

  • Email The user's email address.

  • IsApproved Whether the user has been approved or not.

  • IsLockedOut The user's locked-out status.

  • IsOnline Indicates whether the user is online.

  • LastActivityDate The last time the user performed an action related to membership.

  • LastLockoutDate The last time the user was locked out.

  • LastLoginDate The last time the user logged in.

  • LastPasswordChangedDate The last time the user's password was changed.

  • PasswordQuestion The user's secret question used for password operations.

  • ProviderName The name of the Membership Provider. This always corresponds to the name as defined in Web.config.

  • ProviderUserKey The provider-specific unique identifier for the given user. The actual key depends on which provider you're using.

  • UserName The user's name.

As with all of the schemas for all custom providers in this chapter, I am designing an XML-based custom provider. The quickest and easiest way to get some code up and running that will read and write to and from an XML file while still maintaining a relational view for easy access is to use a typed dataset. The typed dataset, like a regular dataset, can easily persist itself in an XML file. Figure 29.1 shows this typed dataset in the Visual Studio Designer.

Figure 29.1. The MembershipDataSet typed dataset.


Creating a Custom Membership Provider

Now that you've seen the typed dataset that is going to be the XML file in which the custom Membership information will be stored and you've also seen the list of properties and methods that you need to override in order to create your own Membership provideryou can finally get started on the coding.

To get started, create a C# Class Library called XMLProviders. You can delete the Class1.cs file that comes in the library because you have to rename it anyway. Next, make sure that this Class Library has a reference to System.Configuration. If it doesn't have a reference to System.Web, make sure it has that as well.

The basic premise behind this custom Membership provider is that every call to the provider that normally would have connected to a SQL Server database, an Oracle database, or an Access database is going to instead connect to a single XML file on the hard disk of the server. The code in Listing 29.1 contains the first class in this Class Library: MembershipProvider.cs.

Through the magic of partial classes, some of the private utility methods that make the code in Listing 29.1 possible are contained in a separate file called MembershipProvider_Utils.cs, but are still part of the class. These have been omitted from the listing to save room in the chapter, but the methods are available in the code accompanying the book. The methods in that file include InitializeData, SaveData, LoadKey, HexStringToByteArray, ComparePassword, GetClearPassword, ConvertPasswordForStorage, GetUserByName, CopyUserRowToMembershipUser, CopyMembershipUserToRow, QueryUsers (the workhorse of the entire class), and GetUserByNameAndPassword. Most of the encryption code in the utility file is inspired by some of the sample code for a custom Membership provider in the WinFX SDK published by MSDN. You are free to choose your own encryption methods if you like, but the code in this file and in the WinFX SDK samples is quite secure.

Listing 29.1. The XML Membership Provider Class

using System; using System.Configuration; using System.Collections.Generic; using System.Text; using System.Web; using System.Web.Security; using System.Web.Configuration; using System.IO; using System.Data; namespace SAMS.CustomProviders.XML {   public partial class MembershipProvider :   System.Web.Security.MembershipProvider {   private MembershipDataSet memberData;   private string name;   private string memberFile;   private bool enablePasswordReset = false;   private bool enablePasswordRetrieval = false;   private string applicationName;   private byte[] decryptionKey;   private byte[] validationKey;   private bool requiresQuestionAndAnswer = false;   private bool requiresUniqueEmail = false;   private MembershipPasswordFormat passFormat =     MembershipPasswordFormat.Hashed;   private int maxInvalidPasswordAttempts;   private int minRequiredNonAlphanumericCharacters;   private int minRequiredPasswordLength;   private int passwordAttemptWindow;   private string passwordStrengthRegularExpression;   public override void Initialize(string name,     System.Collections.Specialized.NameValueCollection config)   {     this.name = name;     if (config["applicationName"] != null)       applicationName = config["applicationName"];     if (config["enablePasswordRetrieval"] != null)       enablePasswordRetrieval = Convert.ToBoolean( config["enablePasswordRetrieval"]);     if (config["enablePasswordReset"] != null)       enablePasswordReset = Convert.ToBoolean(config["enablePasswordReset"]);     if (config["requiresQuestionAndAnswer"] != null)       requiresQuestionAndAnswer =         Convert.ToBoolean(config["requiresQuestionAndAnswer"]);     if (config["requiresUniqueEmail"] != null)       requiresUniqueEmail =         Convert.ToBoolean(config["requiresUniqueEmail"]);     if (config["maxInvalidPasswordAttempts"] != null)       maxInvalidPasswordAttempts =         Convert.ToInt32(config["maxInvalidPasswordAttempts"]);     if (config["minRequiredNonAlphanumericCharacters"] != null)       minRequiredNonAlphanumericCharacters =         Convert.ToInt32(config["minRequiredNonAlphanumericCharacters"]);     if (config["minRequiredPasswordLength"] != null)       minRequiredPasswordLength = Convert.ToInt32( config["minRequiredPasswordLength"]);     if (config["passwordAttemptWindow"] != null)       passwordAttemptWindow = Convert.ToInt32( config["passwordAttemptWindow"]);     if (config["passwordStrengthRegularExpression"] != null)       passwordStrengthRegularExpression = config["passwordStrengthRegularExpression"];     if (config["passwordFormat"] != null)     {       switch (config["passwordFormat"].ToLower())       {           case "clear":             passFormat = MembershipPasswordFormat.Clear;             break;           case "hashed":             passFormat = MembershipPasswordFormat.Hashed;             break;           case "encrypted":             passFormat = MembershipPasswordFormat.Encrypted;             break;           default:             throw new ConfigurationErrorsException(               string.Format("Unknown password format {0}.",               config["passwordFormat"]));       }     } memberFile =    ConfigurationManager.ConnectionStrings[       config["connectionStringName"]].ConnectionString; InitializeData(); LoadKey(config); } /*  * Various simple get/set properties excluded from code listing * for clarity */ public override bool ChangePassword(string username, string oldPassword, string newPassword) {   MembershipDataSet.UsersRow user = GetUserByName(username);   if (user == null)     throw new InvalidDataException("No such user exists.");   if (!ComparePassword(oldPassword, user.Password))     throw new ApplicationException("Existing password does not match.");   user.Password = ConvertPasswordForStorage(newPassword);   user.LastPasswordChangedTimeStamp = DateTime.Now;   SaveData();   return true; } public override bool ChangePasswordQuestionAndAnswer(string username, string password,   string newPasswordQuestion, string newPasswordAnswer) {    MembershipDataSet.UsersRow user =      GetUserByNameAndPassword(username, password);    user.PasswordQuestion = newPasswordQuestion;    user.PasswordAnswer = newPasswordAnswer;    SaveData();    return true; } public override MembershipUser CreateUser(string username, string password, string email,     string passwordQuestion, string passwordAnswer, bool isApproved,     object providerUserKey, out MembershipCreateStatus status) {   status = MembershipCreateStatus.UserRejected;   MembershipDataSet.UsersRow user = GetUserByName(username);   if (user != null)   {     status = MembershipCreateStatus.DuplicateUserName;     return null;   } if (requiresUniqueEmail) {   MembershipDataSet.UsersRow[] users =     (MembershipDataSet.UsersRow[])memberData.Users.Select(        "Email='" + email + "' AND ApplicationName='" + applicationName + "'");   if ((users != null) && (users.Length > 0))   {     status = MembershipCreateStatus.DuplicateEmail;     return null;   } } Guid newUserId = Guid.NewGuid(); MembershipDataSet.UsersRow newUser = memberData.Users.NewUsersRow(); newUser.UserId = newUserId; newUser.UserName = username; newUser.Password = ConvertPasswordForStorage(password); newUser.Email = email; newUser.ApplicationName = applicationName; newUser.PasswordQuestion = passwordQuestion; newUser.PasswordAnswer = passwordAnswer; newUser.IsApproved = isApproved; newUser.LastActivityTimeStamp = DateTime.Now; newUser.LastLoginTimeStamp = DateTime.Now; newUser.Comment = string.Empty; newUser.UserCreationTimeStamp = DateTime.Now; newUser.LastPasswordChangedTimeStamp = DateTime.Now; memberData.Users.AddUsersRow(newUser); SaveData(); MembershipUser newMembershipUser = new MembershipUser(name,     username, providerUserKey, email, passwordQuestion, string.Empty,     isApproved, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now,     DateTime.MinValue); status = MembershipCreateStatus.Success; return newMembershipUser; } public override bool DeleteUser(string username, bool deleteAllRelatedData) {   MembershipDataSet.UsersRow user = GetUserByName(username);   if (user == null)     throw new ApplicationException("No such user exists.");   user.Delete();   SaveData();   return true; } public override MembershipUserCollection FindUsersByEmail(string emailToMatch,     int pageIndex, int pageSize, out int totalRecords) {     MembershipUserCollection uc = QueryUsers(       "Email='" + emailToMatch + "' AND ApplicationName='" + applicationName + "'",       pageIndex, pageSize, out totalRecords);     return uc; } public override MembershipUserCollection FindUsersByName(string usernameToMatch,     int pageIndex, int pageSize, out int totalRecords) {     MembershipUserCollection uc = QueryUsers(       "UserName='" + usernameToMatch + "' AND ApplicationName='" + applicationName + "'",     pageIndex, pageSize, out totalRecords);     return uc; } public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int     totalRecords) {     MembershipUserCollection uc = QueryUsers(      "ApplicationName='" + applicationName + "'", pageIndex, pageSize, out totalRecords);     return uc; } public override int GetNumberOfUsersOnline() {     int totalRecords;     MembershipUserCollection uc = QueryUsers(     "LastActivityTimeStamp >= " + DateTime.Now.AddMinutes(-1 *     Membership.UserIsOnlineTimeWindow).ToString(),     0, 1, out totalRecords);     return totalRecords; } public override string GetPassword(string username, string answer) {     if ((!enablePasswordRetrieval) ||     (passFormat == MembershipPasswordFormat.Hashed))       throw new        ApplicationException(          "Current configuration settings prevent password retrieval.");     MembershipDataSet.UsersRow user = GetUserByName(username);     if (user == null)       throw new ApplicationException("No such user exists.");     if (requiresQuestionAndAnswer)     {       if (user.PasswordAnswer.ToUpper() != answer.ToUpper())       {          throw new ApplicationException(            "Security question answer supplied is incorrect.");       }     }     return GetClearPassword(user.Password); } public override MembershipUser GetUser(string username, bool userIsOnline) {   MembershipDataSet.UsersRow user = GetUserByName(username);   MembershipUser mu;   if (userIsOnline)   {     user.LastActivityTimeStamp = DateTime.Now;     SaveData();   }   mu = new MembershipUser(     name, user.UserName, user.UserId,     user.Email, user.PasswordQuestion, user.Comment,     user.IsApproved, false, user.UserCreationTimeStamp, user.LastLoginTimeStamp,     user.LastActivityTimeStamp, user.LastPasswordChangedTimeStamp, DateTime.MinValue);   return mu; } public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) {   DataRow[] rows =     memberData.Users.Select(       "UserId='" + ((Guid)providerUserKey).ToString() + "'");   if ((rows != null) && (rows.Length > 0))   {     MembershipDataSet.UsersRow user = (MembershipDataSet.UsersRow)rows[0];   MembershipUser mu;   if (userIsOnline)   {     user.LastActivityTimeStamp = DateTime.Now;     SaveData();   }   mu = new MembershipUser(         name, user.UserName, user.UserId,         user.Email, user.PasswordQuestion, user.Comment,         user.IsApproved, false, user.UserCreationTimeStamp, user.LastLoginTimeStamp,         user.LastActivityTimeStamp, user.LastPasswordChangedTimeStamp,         DateTime.MinValue);   return mu; } else {   throw new ApplicationException("Specified user does not exist."); } } public override string GetUserNameByEmail(string email) {   MembershipDataSet.UsersRow user;   DataRow[] rows =     memberData.Users.Select(     "Email='" + email + "'" +     " AND ApplicationName='" + applicationName + "'");   if ((rows != null) && (rows.Length > 0))   {      user = (MembershipDataSet.UsersRow)rows[0];      return user.UserName;   }   else     throw new ApplicationException( "No such user exists with given e-mail address."); } public override string ResetPassword(string username, string answer) {   if (!enablePasswordReset)     throw new ApplicationException(      "Cannot reset password under current provider configuration settings.");   MembershipDataSet.UsersRow user = GetUserByName(username);   if (user == null)    throw new ApplicationException("No such user found.");   string newPw =     Membership.GeneratePassword(minRequiredPasswordLength,       minRequiredNonAlphanumericCharacters);   user.Password = ConvertPasswordForStorage(newPw);   user.LastPasswordChangedTimeStamp = DateTime.Now;   SaveData();   return newPw; } public override bool UnlockUser(string userName) {   // this provider doesn't lock users out, so just do nothing.   return true; } public override void UpdateUser(MembershipUser user) {   DataRow[] rows =     memberData.Users.Select(       string.Format("UserId={0}", ((Guid)user.ProviderUserKey).ToString()));   if ((rows !=null) && (rows.Length > 0))   {     MembershipDataSet.UsersRow userRow =      (MembershipDataSet.UsersRow)rows[0];     CopyMembershipUserToRow(user, userRow);     SaveData();   } else     throw new ApplicationException("No such user found to update."); } public override bool ValidateUser(string username, string password) {   MembershipDataSet.UsersRow user = GetUserByName(username);   if (user == null)     return false;   if (!ComparePassword(password, user.Password))     return false;   user.LastLoginTimeStamp = DateTime.Now;   user.LastActivityTimeStamp = DateTime.Now;   SaveData();   return true; } } } 

Configuring and Installing the Membership Provider

After you have created the Membership Provider class and your XMLProviders Class Library compiles without a hitch, you need to configure your provider for use with an ASP.NET application. This is done through a Web.config setting called <membership>. You saw a little bit about this element in Chapter 28 where you learned how to use the Membership provider. The following is a sample <membership> element element (Web.config files);Membership provider, configuring> that is configured to use the custom XML membership provider created in Listing 29.1:

<membership defaultProvider="xmlMembership" userIsOnlineTimeWindow="15"> <providers> <add name="xmlMembership"     type="SAMS.CustomProviders.XML.MembershipProvider"     connectionStringName="membershipProvider"     enablePasswordRetrieval="false"     enablePasswordReset="true"     requireQuestionAndAnswer="true"     applicationName="CustomProviderDemo"     requiresUniqueEmail="false"     passwordFormat="Hashed"     minRequiredNonAlphanumericCharacters="2"     minRequiredPasswordLength="2"     maxInvalidPasswordAttempts="3"     passwordAttemptWindow="30"     passwordStrengthRegularExpression=""     description="Stores and Retrieves membership data from an XML file"     decryptionKey="34a266624e967adf6e92937c5341e931e73f25fef798ba75"     validationKey="34a31f547c659b6e35edc029dd3abbe42f8936      cb2b24fff3e1bef13be429505b3f5becb5702e15bc7b98cd6fd2b7702      e46ff63fdc9ea8979f6508c82638b129a"/> </providers> </membership> 


The first important attribute is the type attribute. This tells ASP.NET where to find the class that derives from MembershipProvider, as that class will be instantiated the first time any Membership operations are performed in the application. In the case of my sample provider, the type is SAMS.CustomProviders.XML.MembershipProvider. In order for this to work, the web application needs to have a copy of the Assembly in which that type is defined in its bin directory. The easy way to do this is just to make sure that the web application references the XMLProviders project. If you are deploying your providers without regard to application, you can install them in the Global Assembly Cache (GAC), in which case you would need to supply a fully qualified type name including the type name, Assembly name, version number, and culture identifier for the type attribute.



Microsoft Visual C# 2005 Unleashed
Microsoft Visual C# 2005 Unleashed
ISBN: 0672327767
EAN: 2147483647
Year: 2004
Pages: 298

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net