Monday, July 28, 2008

Advantage Membership Provider

This post is one of a series of posts about the Advantage Providers for ASP.NET other posts in this series are listed below.

ASP.NET includes a support for the Provider Model which contains a definition for a Membership Provider. This provider allows storage of user information into various forms. In this case the Advantage Membership Provider stores the user/member information in an Advantage Database.

The Membership Provider is invoked through the Membership class which will then communicate with Advantage (for details about the data structure see this post). Implementation of the provider is pretty straight forward and I already showed some of the functionality in my posts on Securely Storing Passwords and Using Salt. In this post I will discuss several of the public functions of the provider.

A Membership Provider has several properties which can be used to change the behavior of the provider. These properties are set in the Web.Config file when defining which provider to use. Most of these properties are used to determine password rules. The PasswordFormat property determines how the password is stored in the database valid options include; Clear, Encrypted and Hashed. There are also properties for allowing password reset (EnablePasswordReset), password length (MinRequiredPasswordLength) and the number of non-alphanumeric characters(MinRequiredNonAlphanumericCharacters). Additional information about these properties can be found here. Fortunately most of the properties do not require any special implementation to work with Advantage.

Nearly all of the public methods of the provider had to be overridden to be used with Advantage since these methods read and write to the data store. Direct user manipulation includes creating, updating, finding and deleting. Other functions include resetting passwords, unlocking and validating users. Descriptions for all of the public methods can be found here

The CreateUser function takes the provided user information and inserts a new record into the Membership table using the provided information. It outputs a MembershipCreateStatus which reports any problems with creating the user. The Advantage provider uses a GUID as the unique identifier for each user and also has a unique index on the username. The CreateUser function does a series of checks to ensure that the new user is unique before inserting the new record. A code sample is below:

   1: public override MembershipUser CreateUser(string username,
   2:              string password,
   3:              string email,
   4:              string passwordQuestion,
   5:              string passwordAnswer,
   6:              bool isApproved,
   7:              object providerUserKey,
   8:              out MembershipCreateStatus status)
   9:     {
  10:         ValidatePasswordEventArgs args =
  11:           new ValidatePasswordEventArgs(username, password, true);
  12:  
  13:         OnValidatingPassword(args);
  14:  
  15:         if (args.Cancel)
  16:         {
  17:             status = MembershipCreateStatus.InvalidPassword;
  18:             return null;
  19:         }
  20:  
  21:         if (RequiresUniqueEmail && GetUserNameByEmail(email) != "")
  22:         {
  23:             status = MembershipCreateStatus.DuplicateEmail;
  24:             return null;
  25:         }
  26:  
  27:         MembershipUser u = GetUser(username, false);
  28:  
  29:         if (u == null)
  30:         {
  31:             DateTime createDate = DateTime.Now;
  32:  
  33:             if (providerUserKey == null)
  34:             {
  35:                 providerUserKey = Guid.NewGuid();
  36:             }
  37:             else
  38:             {
  39:                 if (!(providerUserKey is Guid))
  40:                 {
  41:                     status = MembershipCreateStatus.InvalidProviderUserKey;
  42:                     return null;
  43:                 }
  44:             }
  45:  
  46:             AdsConnection conn = new AdsConnection(connectionString);
  47:             AdsCommand cmd = new AdsCommand("INSERT INTO Membership " +
  48:                   " (UserID, Username, Password, Email, PasswordQuestion, " +
  49:                   " PasswordAnswer, IsApproved," +
  50:                   " Comment, CreationDate, LastPasswordChangedDate, LastActivityDate," +
  51:                   " ApplicationID, IsLockedOut, LastLockedOutDate," +
  52:                   " FailedPasswordAttemptCount, FailedPasswordAttemptWindowStart, " +
  53:                   " FailedPasswordAnswerAttemptCount, FailedPasswordAnswerAttemptWindowStart)" +
  54:                   " Values(:UserID, :Username, :Password, :Email, :PasswordQuestion, :PasswordAnswer, " +
  55:                   ":IsApproved, :Comment, :CreationDate, :LastPasswordChangedDate, :LastActivityDate, " +
  56:                   ":ApplicationID, :IsLockedOut, :LastLockedOutDate, :FailedPasswordAttemptCount, " +
  57:                   ":FailedPasswordAttemptWindowStart, :FailedPasswordAnswerAttemptCount, :FailedPasswordAnswerAttemptWindowStart)", conn);
  58:  
  59:             cmd.Parameters.Add("UserID", System.Data.DbType.String).Value = providerUserKey.ToString();
  60:             cmd.Parameters.Add("Username", System.Data.DbType.String).Value = username;
  61:             cmd.Parameters.Add("Password", System.Data.DbType.String).Value = EncodePassword(password, providerUserKey.ToString());
  62:             cmd.Parameters.Add("Email", System.Data.DbType.String).Value = email;
  63:             cmd.Parameters.Add("PasswordQuestion", System.Data.DbType.String).Value = passwordQuestion;
  64:             cmd.Parameters.Add("PasswordAnswer", System.Data.DbType.String).Value = EncodePassword(passwordAnswer, providerUserKey.ToString());
  65:             cmd.Parameters.Add("IsApproved", System.Data.DbType.Boolean).Value = isApproved;
  66:             cmd.Parameters.Add("Comment", System.Data.DbType.String).Value = "";
  67:             cmd.Parameters.Add("CreationDate", System.Data.DbType.DateTime).Value = createDate;
  68:             cmd.Parameters.Add("LastPasswordChangedDate", System.Data.DbType.DateTime).Value = createDate;
  69:             cmd.Parameters.Add("LastActivityDate", System.Data.DbType.DateTime).Value = createDate;
  70:             cmd.Parameters.Add("ApplicationID", System.Data.DbType.String).Value = pApplicationID;
  71:             cmd.Parameters.Add("IsLockedOut", System.Data.DbType.Boolean).Value = false;
  72:             cmd.Parameters.Add("LastLockedOutDate", System.Data.DbType.DateTime).Value = createDate;
  73:             cmd.Parameters.Add("FailedPasswordAttemptCount", System.Data.DbType.Int32).Value = 0;
  74:             cmd.Parameters.Add("FailedPasswordAttemptWindowStart", System.Data.DbType.DateTime).Value = createDate;
  75:             cmd.Parameters.Add("FailedPasswordAnswerAttemptCount", System.Data.DbType.Int32).Value = 0;
  76:             cmd.Parameters.Add("FailedPasswordAnswerAttemptWindowStart", System.Data.DbType.DateTime).Value = createDate;
  77:  
  78:             try
  79:             {
  80:                 conn.Open();
  81:  
  82:                 int recAdded = cmd.ExecuteNonQuery();
  83:  
  84:                 if (recAdded > 0)
  85:                 {
  86:                     status = MembershipCreateStatus.Success;
  87:                 }
  88:                 else
  89:                 {
  90:                     status = MembershipCreateStatus.UserRejected;
  91:                 }
  92:             }
  93:             catch (AdsException e)
  94:             {
  95:                 if (WriteExceptionsToEventLog)
  96:                 {
  97:                     WriteToEventLog(e, "CreateUser");
  98:                 }
  99:  
 100:                 status = MembershipCreateStatus.ProviderError;
 101:             }
 102:             finally
 103:             {
 104:                 conn.Close();
 105:             }
 106:  
 107:  
 108:             return GetUser(username, false);
 109:         }
 110:         else
 111:         {
 112:             status = MembershipCreateStatus.DuplicateUserName;
 113:         }
 114:  
 115:  
 116:         return null;
 117:     }

Users are updated in a similar fashion using the UpdateUser method. The user is found by providing a MembershipUser object to the function. You can obtain the MembershipUser object by using the one of the GetUser functions. Other helper functions also write to the user record. For example the LastLoginDate is updated by the ValidateUser function when a user successfully logs into the system. Alternately the FailedPasswordAttemptCount gets incremented along with the FailedPasswordAttemptWindowStart if the password is invalid.

The following code demonstrates the implementation of the GetUser public method. The user information is read from the database and and loaded into a DataReader object. A helper function, GetUserFromReader,  inserts the user information into a MembershipUser object which is then returned. The method also updates the LastActivityDate in the database.

   1: public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
   2: {
   3:     AdsConnection conn = new AdsConnection(connectionString);
   4:     AdsCommand cmd = new AdsCommand("SELECT UserID, Username, Email, PasswordQuestion," +
   5:           " Comment, IsApproved, IsLockedOut, CreationDate, LastLoginDate," +
   6:           " LastActivityDate, LastPasswordChangedDate, LastLockedOutDate" +
   7:           " FROM Membership WHERE UserID = :UserID", conn);
   8:  
   9:     cmd.Parameters.Add("UserID", System.Data.DbType.String).Value = providerUserKey;
  10:  
  11:     MembershipUser u = null;
  12:     AdsDataReader reader = null;
  13:  
  14:     try
  15:     {
  16:         conn.Open();
  17:  
  18:         reader = cmd.ExecuteReader();
  19:  
  20:         if (reader.HasRows)
  21:         {
  22:             reader.Read();
  23:             u = GetUserFromReader(reader);
  24:  
  25:             if (userIsOnline)
  26:             {
  27:                 AdsCommand updateCmd = new AdsCommand("UPDATE Membership " +
  28:                           "SET LastActivityDate = :LastActivityDate " +
  29:                           "WHERE UserID = :UserID", conn);
  30:  
  31:                 updateCmd.Parameters.Add("LastActivityDate", System.Data.DbType.DateTime).Value = DateTime.Now;
  32:                 updateCmd.Parameters.Add("UserID", System.Data.DbType.String).Value = providerUserKey;
  33:  
  34:                 updateCmd.ExecuteNonQuery();
  35:             }
  36:         }
  37:  
  38:     }
  39:     catch (AdsException e)
  40:     {
  41:         if (WriteExceptionsToEventLog)
  42:         {
  43:             WriteToEventLog(e, "GetUser(Object, Boolean)");
  44:  
  45:             throw new ProviderException(exceptionMessage);
  46:         }
  47:         else
  48:         {
  49:             throw e;
  50:         }
  51:     }
  52:     finally
  53:     {
  54:         if (reader != null) { reader.Close(); }
  55:  
  56:         conn.Close();
  57:     }
  58:  
  59:     return u;
  60: }
   1: private MembershipUser GetUserFromReader(AdsDataReader reader)
   2: {
   3:     object providerUserKey = reader.GetValue(0);
   4:     string username = reader.GetString(1);
   5:     string email = reader.GetString(2);
   6:  
   7:     string passwordQuestion = "";
   8:     if (reader.GetValue(3) != DBNull.Value)
   9:         passwordQuestion = reader.GetString(3);
  10:  
  11:     string comment = "";
  12:     if (reader.GetValue(4) != DBNull.Value)
  13:         comment = reader.GetString(4);
  14:  
  15:     bool isApproved = reader.GetBoolean(5);
  16:     bool isLockedOut = reader.GetBoolean(6);
  17:     DateTime creationDate = reader.GetDateTime(7);
  18:  
  19:     DateTime lastLoginDate = new DateTime();
  20:     if (reader.GetValue(8) != DBNull.Value)
  21:         lastLoginDate = reader.GetDateTime(8);
  22:  
  23:     DateTime lastActivityDate = reader.GetDateTime(9);
  24:     DateTime lastPasswordChangedDate = reader.GetDateTime(10);
  25:  
  26:     DateTime lastLockedOutDate = new DateTime();
  27:     if (reader.GetValue(11) != DBNull.Value)
  28:         lastLockedOutDate = reader.GetDateTime(11);
  29:  
  30:     MembershipUser u = new MembershipUser(this.Name,
  31:                                           username,
  32:                                           providerUserKey,
  33:                                           email,
  34:                                           passwordQuestion,
  35:                                           comment,
  36:                                           isApproved,
  37:                                           isLockedOut,
  38:                                           creationDate,
  39:                                           lastLoginDate,
  40:                                           lastActivityDate,
  41:                                           lastPasswordChangedDate,
  42:                                           lastLockedOutDate);
  43:  
  44:     return u;
  45: }

The ASP.NET Login Controls interact with the provider to obtain user information. For example; the LoginControl uses the  ValidateUser method to verify that the correct username and password are correct. The CreateUserWizard uses the CreateUser event to create a new user and so on.

You can download the provider from CodeCentral on the DevZone under the WebApps category. You can also use this direct link to the Advantage ASP.NET Providers project. I'll be discussing the Role Provider next.

1 comment:

Newbie said...

Hi,
Thanks for the articles, give me several new perspectives.
One thing that I want to know, is it possible to make the membership tables in the same Dictionary with the one that used by the application?