
Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
878 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
First, you have to verify whether any configuration is passed in. If nothing is configured for the provider, it won’t work. Second, if no name is specified, you have to initialize a default name, which is required by the configuration tool for displaying the provider in the list of providers. Finally, you have to add a default description if no description is configured for the provider. This final step is optional but useful for configuration tools that query provider information.
Don’t forget to call the base class’s Initialize implementation for initializing basic properties properly. You do this in the last line of code in the previous code.
Next, you can start initializing your properties:
...
// Initialize default values _ApplicationName = "DefaultApp"; _EnablePasswordReset = false;
_PasswordStrengthRegEx = @"[\w| !§$%&/()=\-?\*]*"; _MaxInvalidPasswordAttempts = 3; _MinRequiredNonAlphanumericChars = 1; _MinRequiredPasswordLength = 5; _RequiresQuestionAndAnswer = false; _PasswordFormat = MembershipPasswordFormat.Hashed;
// Now go through the properties and initialize custom values foreach (string key in config.Keys)
{
switch(key.ToLower())
{
case "name":
_Name = config[key]; break;
case "applicationname": _ApplicationName = config[key]; break;
case "filename":
_FileName = config[key]; break;
case "enablepasswordreset":
_EnablePasswordReset = bool.Parse(config[key]); break;
case "passwordstrengthregex": _PasswordStrengthRegEx = config[key]; break;
case "maxinvalidpasswordattempts": _MaxInvalidPasswordAttempts = int.Parse(config[key]); break;
case "minrequirednonalphanumericchars": _MinRequiredNonAlphanumericChars = int.Parse(config[key]); break;
case "minrequiredpasswordlength": _MinRequiredPasswordLength = int.Parse(config[key]); break;
case "passwordformat":
_PasswordFormat = (MembershipPasswordFormat)Enum.Parse( typeof(MembershipPasswordFormat), config[key]);
break;
case "requiresquestionandanswer": _RequiresQuestionAndAnswer = bool.Parse(config[key]); break;
}

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
879 |
■Caution In our first implementation, we tried to derive the default application name from the current HTTP context automatically based on the virtual root directory. The effect was that our provider worked properly as long as we used the management functions from within the application. As soon as we tried to use it from the ASP.NET WAT, though, it failed with an exception. When debugging, we discovered that in this case the provider doesn’t have access to members of the application’s HTTP context. Therefore, you should avoid using the HttpContext.Current in your Membership provider and instead keep it as simple as possible.
The previous code starts by initializing some default values for your options, just in case they are not included in the web.config configuration file. After initializing these default values, you can go through the entries of the config parameter passed into the method (which is a simple NameValueCollection). As you can see, you even can include custom settings such as the filename setting, which is not included in the default set of properties of the Membership provider. This filename property is a custom property for your specific provider that points to the XML file that contains the user information. You will pass this filename to the UserStore class in a separate property that you will use in the remaining functions of the implementation.
private UserStore CurrentStore
{
get
{
if (_CurrentStore == null)
_CurrentStore = UserStore.GetStore(_FileName); return _CurrentStore;
}
}
Next, you have a large number of methods in your provider. These methods are for creating, updating, and deleting users as well as for accessing and retrieving user details. The methods basically access the information through the previously created store classes.
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion,
string passwordAnswer, bool isApproved,
object providerUserKey, out MembershipCreateStatus status) public override bool DeleteUser(string username, bool deleteAllRelatedData) public override MembershipUser GetUser(string username, bool userIsOnline)
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
public override string GetUserNameByEmail(string email) public override void UpdateUser(MembershipUser user)
public override bool ValidateUser(string username, string password) public override bool ChangePassword(string username,
string oldPassword, string newPassword)
public override bool ChangePasswordQuestionAndAnswer(string username,
string password, string newPasswordQuestion, string newPasswordAnswer) public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
public override MembershipUserCollection FindUsersByName( string usernameToMatch,
int pageIndex, int pageSize, out int totalRecords) public override MembershipUserCollection GetAllUsers(int pageIndex,
int pageSize, out int totalRecords) public override int GetNumberOfUsersOnline()

880 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
public override string GetPassword(string username, string answer) public override string ResetPassword(string username, string answer) public override bool UnlockUser(string userName)
Within those methods, you just have to call the appropriate methods of the UserStore class through the previously introduced CurrentStore property. These are the only methods defined by the provider. Any additional method introduced in this chapter is a helper method that you have to include on your own. (In this book, you will see the most important implementations of these methods but not all of them. The complete code is available with the book’s download.)
Let’s get started with the CreateUser method.
Creating Users and Adding Them to the Store
The CreateUser method is interesting because it needs to make sure that the user name and e-mail are unique and that the password is valid and adheres to the password strength requirements.
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion,
string passwordAnswer, bool isApproved,
object providerUserKey, out MembershipCreateStatus status)
{
try
{
// Validate the username and email
if (!ValidateUsername(username, email, Guid.Empty))
{
status = MembershipCreateStatus.InvalidUserName; return null;
}
//Raise the event before validating the password base.OnValidatingPassword(
new ValidatePasswordEventArgs( username, password, true));
//Validate the password
if (!ValidatePassword(password))
{
status = MembershipCreateStatus.InvalidPassword; return null;
}
...
In the first section, the function calls the private methods ValidateUserName and ValildatePassword. These methods make sure the user name and e-mail are unique in the store and the password adheres to the password strength requirements. After these checks succeed, you can create the user for the underlying store (SimpleUser), add the user to the store, and then save the store.
...
// Everything is valid, create the user SimpleUser user = new SimpleUser(); user.UserKey = Guid.NewGuid(); user.UserName = username;
user.Password = this.TransformPassword(password); user.Email = email;
user.PasswordQuestion = passwordQuestion; user.PasswordAnswer = passwordAnswer;

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
881 |
user.CreationDate = DateTime.Now; user.LastActivityDate = DateTime.Now; user.LastPasswordChangeDate = DateTime.Now;
// Add the user to the store CurrentStore.Users.Add(user); CurrentStore.Save();
status = MembershipCreateStatus.Success; return CreateMembershipFromInternalUser(user);
}
catch
{
throw;
}
}
Finally, the method needs to return an instance of MembershipUser to the calling Membership class with the details of the created user. For this purpose, you just need to match the properties
of your SimpleUser instance to the properties of the MembershipUser, as shown in the following function:
private MembershipUser CreateMembershipFromInternalUser(SimpleUser user)
{
MembershipUser muser = new MembershipUser(base.Name, user.UserName, user.UserKey, user.Email, user.PasswordQuestion,
string.Empty, true, false, user.CreationDate, user.LastLoginDate, user.LastActivityDate, user.LastPasswordChangeDate, DateTime.MaxValue);
return muser;
}
As you can see, this mapping creates an instance of MembershipUser and passes the appropriate properties from your own SimpleUser as constructor parameters.
Next, take a look at the validation functions for validating the user name, e-mail, and password:
private bool ValidatePassword(string password)
{
bool IsValid = true; Regex HelpExpression;
// Validate simple properties
IsValid = IsValid && (password.Length >= this.MinRequiredPasswordLength);
//Validate non-alphanumeric characters HelpExpression = new Regex(@"\W"); IsValid = IsValid && (
HelpExpression.Matches(password).Count >= this.MinRequiredNonAlphanumericCharacters);
//Validate regular expression
HelpExpression = new Regex(this.PasswordStrengthRegularExpression);
IsValid = IsValid && (HelpExpression.Matches(password).Count > 0);
return IsValid;
}

882 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
The password validation first verifies the length of the password. If the password is too short, it returns false. It then verifies through the .NET Framework regular expression classes whether the number of nonalphanumeric characters in the password is high enough according to the MinRequireNonAlphanumericCharacters and then validates the password again through regular expressions against the PasswordStrengthRegularExpression. If all these checks pass, the function returns true. If these checks don’t pass, it returns false.
Now let’s take a closer look at the method for validating the user name and the e-mail. Both need to be unique in the underlying store.
private bool ValidateUsername(string userName, string email, Guid excludeKey)
{
bool IsValid = true;
UserStore store = UserStore.GetStore(_FileName); foreach (SimpleUser user in store.Users)
{
if (user.UserKey.CompareTo(excludeKey) != 0)
{
if (string.Equals(user.UserName, userName, StringComparison.OrdinalIgnoreCase))
{
IsValid = false; break;
}
if (string.Equals(user.Email, email, StringComparison.OrdinalIgnoreCase))
{
IsValid = false; break;
}
}
}
return IsValid;
}
As you can see in the previous snippet, user validation is fairly simple. The code goes through the users in the CurrentStore and verifies whether there is any user with the same user name or e-mail. If that’s the case, the function returns false or otherwise true. The last interesting part in the CreateUser method is how the password is set for the user. Through the PasswordFormat property, every provider has three types for storing the password: clear, hashed, and encrypted. The CreateUser method uses a private helper method of the XmlMembershipProvider class called TransFormPassword, as follows:
user.Password = this.TransformPassword(password);
This method queries the current setting for the PasswordFormat property, and according to the setting it leaves the password as clear text, creates a hash for the password, or encrypts the password, as follows:
private string TransformPassword(string password)
{
string ret = string.Empty;
switch (PasswordFormat)
{

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
883 |
case MembershipPasswordFormat.Clear: ret = password;
break;
case MembershipPasswordFormat.Hashed:
ret = FormsAuthentication.HashPasswordForStoringInConfigFile( password, "SHA1");
break;
case MembershipPasswordFormat.Encrypted:
byte[] ClearText = Encoding.UTF8.GetBytes(password); byte[] EncryptedText = base.EncryptPassword(ClearText); ret = Convert.ToBase64String(EncryptedText);
break;
}
return ret;
}
If the password format is set to Clear, it just returns the clear-text password. In the case of the Hashed setting, it creates the simple hash through the forms authentication utility method and then returns the hash for the password. The last possible option stores the password encrypted in the database, which has the advantage that the password can be retrieved from the database through decryption. In that case, the method uses the EncryptPassword method from the base class implementation for encrypting the password. This method uses a key stored in machine.config for encrypting the password. If you are using this in a web farm environment, you have to sync the key stored in machine.config on every machine so that a password encrypted on one machine of the farm can be decrypted on another machine on the web farm properly.
Validating Users on Login
The Membership class supports a method for programmatically validating a password entered by a user. This method is used by the Login control as well. This means every time the user tries to log in, the ValidateUser method of the Membership class is involved. This method on its own calls the ValidateUser method of the underlying Membership provider. According to the settings of the PasswordFormat property, it has to retrieve the user from the store based on the user name and then somehow validate the password. If the password is clear text, validating the password involves a simple string comparison. Encrypted passwords have to be decrypted and compared afterward, while last but not least validating hashed passwords means re-creating the hash and then comparing the hash values.
public override bool ValidateUser(string username, string password)
{
try
{
SimpleUser user = CurrentStore.GetUserByName(username); if(user == null)
return false;
if (ValidateUserInternal(user, password))
{
user.LastLoginDate = DateTime.Now; user.LastActivityDate = DateTime.Now; CurrentStore.Save();
return true;
}
else
{

884 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
return false;
}
}
catch
{
throw;
}
}
This method retrieves the user from the store. It then validates the password against the password passed in (which is the one entered by the user for login) through a private helper method called ValidateUserInternal. Finally, if the user name and password are fine, it updates the LastLoginDate and the LastActivityDate for the user and then returns true. It’s always useful to encapsulate password validation functionality into a separate function, because it may be used more than once in your provider. A typical example for reusing this functionality is the ChangePassword method where the user has to enter the old password and the new password. If validation of the old password fails, the provider should not change the password, as shown here:
public override bool ChangePassword(string username,
string oldPassword, string newPassword)
{
try
{
// Get the user from the store
SimpleUser user = CurrentStore.GetUserByName(username); if(user == null)
throw new Exception("User does not exist!")
if (ValidateUserInternal(user, oldPassword))
{
// Raise the event before validating the password base.OnValidatingPassword(
new ValidatePasswordEventArgs( username, newPassword, false));
if (!ValidatePassword(newPassword)) throw new ArgumentException(
"Password doesn't meet password strength requirements!");
user.Password = TransformPassword(newPassword); user.LastPasswordChangeDate = DateTime.Now; CurrentStore.Save();
return true;
}
return false;
}
catch
{
throw;
}
}
Only if the old password is entered correctly by the user does the change take place. The ChangePassword method again uses the TransformPassword method to generate the protected

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
885 |
version (hashed, encrypted) of the password if necessary. You can reuse the function introduced previously with the CreateUser method. But now let’s take a look at the password validation functionality:
private bool ValidateUserInternal(SimpleUser user, string password)
{
if (user != null)
{
string passwordValidate = TransformPassword(password); if (string.Compare(passwordValidate, user.Password) == 0)
{
return true;
}
}
return false;
}
This method uses the TransformPassword method for creating the protected version of the password (hashed, encrypted) if necessary. The results are then compared through simple string comparison. (Even the encrypted version returns a Base64-encoded string that will be stored in the XML file; therefore, string comparison is fine.) This is why validating hashed passwords works at all, for example. Just re-create the hash, and then compare the hashed version of the password.
Using Salted Password Hashes
If you want to change this to include a salt value as mentioned, you have to complete the following steps:
1.Add a new field to your SimpleUser class called PasswordSalt.
2.Extend your TransformPassword method to accept a salt value. This salt is necessary for re-creating the hash, which actually will be based on both the password and the salt.
3.When creating a new password, you simply have to create the random salt value and then store it with your user. For any validation, pass the previously generated salt value to the TransformPassword function for validation.
The best way to do this is to extend the TransformPassword so that it generates the salt value automatically if necessary. Therefore, it accepts the salt as a second parameter. This parameter is not just a simple parameter—it’s a reference parameter, as shown here:
private string TransformPassword(string password, ref string salt)
{
...
}
Whenever you pass in string.empty or null for the salt value, the function automatically generates a new salt. The method therefore is called as follows from other methods that create the new password hash. These methods are CreateUser, ChangePassword, and ResetPassword, as they all update the password value of your SimpleUser class.
SimpleUser user = ...
...
user.PasswordSalt = string.Empty;
user.Password = this.TransformPassword(password, ref user.PasswordSalt);
...

886 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
This means every method that updates the password field of your user store sets the PasswordSalt value to string.Empty before it calls TransformPassword and passes in a reference to the user.PasswordSalt field. When validating the password, you don’t want the method to regenerate a new salt value. Therefore, you have to pass in the salt value stored with the hashed version of the password in the data store. Having said that, the previously introduced ValidateUserInternal() method now looks like this:
private bool ValidateUserInternal(SimpleUser user, string password)
{
if (user != null)
{
string passwordValidate = TransformPassword( password, ref user.PasswordSalt);
if (string.Compare(passwordValidate, user.Password) == 0)
{
return true;
}
}
return false;
}
The only thing that changes compared to the original version is that the method now passes in an initialized version of the salt value that will be used by the TransformPassword method to regenerate the password hash based on the existing salt and the password entered by the user. Therefore, internally the TransformPassword method now looks as follows for validating and optionally generating a salt value:
private string TransformPassword(string password, ref string salt)
{
string ret = string.Empty;
switch (PasswordFormat)
{
case MembershipPasswordFormat.Clear: ret = password;
break;
case MembershipPasswordFormat.Hashed:
// Generate the salt if not passed in if (string.IsNullOrEmpty(salt))
{
byte[] saltBytes = new byte[16];
RandomNumberGenerator rng = RandomNumberGenerator.Create(); rng.GetBytes(saltBytes);
salt = Convert.ToBase64String(saltBytes);
}
ret = FormsAuthentication.HashPasswordForStoringInConfigFile( (salt + password), "SHA1");
break;
case MembershipPasswordFormat.Encrypted:
byte[] ClearText = Encoding.UTF8.GetBytes(password); byte[] EncryptedText = base.EncryptPassword(ClearText); ret = Convert.ToBase64String(EncryptedText);

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
887 |
break;
}
return ret;
}
When the provider is configured for storing the passwords as salted hashes, it verifies whether the passed-in salt value is empty or null. If the provider is configured for using salted hashes, it generates a new salt value using the cryptographic random number generator of the System.Security.Cryptography namespace to generate a real random number. The functions CreateUser, ChangePassword, and ResetPassword will pass in null or string.empty to generate a new salt value, while the ValidateUserInternal method passes in the already initialized salt value from the underlying data store of the provider. Afterward, the method again uses the HashPasswordForStoringInConfigFile, but this time it passes a combination of the random salt value and the actual password. The result is returned to the caller.
The Remaining Functions of the Provider
Initializing the provider and creating and validating users are the most important and hardest functions to implement in the provider. The rest of the functions are just for reading information from the store and for updating the users in the store. Basically, these functions call the underlying methods of the UserStore class or try to find users in the UserStore.Users collection. A typical example is the GetUser() method, which retrieves a single user from the data store based on its user name or key:
public override MembershipUser GetUser(string username, bool userIsOnline)
{
try
{
SimpleUser user = CurrentStore.GetUserByName(username); if (user != null)
{
if (userIsOnline)
{
user.LastActivityDate = DateTime.Now; CurrentStore.Save();
}
return CreateMembershipFromInternalUser(user);
}
else
{
return null;
}
}
catch
{
throw;
}
}
This example accepts the name of the user as a parameter and another parameter that indicates whether the user is online. This parameter is automatically initialized by the Membership class when it calls your provider’s method. In your method, you can query this parameter; if it is set to true, you must update the LastActivityDate of your user in the store. The function does nothing other than find the user in the underlying store by calling the UserStore’s GetUserByName method. It then creates an instance of MembershipUser based on the information of the store by calling the