
Asp Net 2.0 Security Membership And Role Management
.pdf
Chapter 11
|
dbo.vw_aspnet_Users |
u |
where |
a.LoweredApplicationName |
= LOWER(@pApplicationName) |
and |
a.ApplicationId |
= u.ApplicationId |
and |
u.LoweredUserName |
= LOWER(@pUserName) |
if not exists (select 1 from dbo.vw_aspnet_MembershipUsers where UserId = @UserId)
return -1
begin transaction
select 1
from vw_aspnet_MembershipUsers WITH (UPDLOCK) where UserId = @UserId
if (@@Error <> 0) goto AnErrorOccurred
insert into dbo.PasswordHistory
values (@UserId,@pPassword,@pPasswordSalt,getutcdate()) if (@@Error <> 0)
goto AnErrorOccurred
--trim away old password records that are no longer needed |
|
delete |
|
from |
dbo.PasswordHistory |
where |
UserId = @UserId |
and |
CreateDate not in |
( |
|
select TOP 10 CreateDate --only 10 passwords are ever maintained in history from dbo.PasswordHistory
where UserId = @UserId order by CreateDate DESC
)
if (@@Error <> 0) goto AnErrorOccurred
commit transaction
return 0
AnErrorOccurred: rollback transaction return -1
The parameter signature for the stored procedure expects a username and an application name — the object-level primary key of any user in Membership. The stored procedure converts these two parameters into the GUID UserId by querying the application and user table views as shown earlier in the chapter. The procedure also makes a sanity check to ensure that the UserId actually exists in the Membership table by querying its associated view. Technically, this should never occur because the custom provider only calls this stored procedure after the base SqlMembershipProvider has created a user row in the aspnet_Membership table.
442

SqlMembershipProvider
After the procedure knows that the UserId is valid, it starts a transaction and places a lock on the user’s Membership record. This ensures that, on the off chance that multiple calls are made to the database to insert a history record for a single user, each call completes its work before another call is allowed to manipulate the PasswordHistory table. This serialization is needed because after the data from the procedure’s password and password salt parameter are inserted, the procedure removes old history records. The procedure needs to complete both steps successfully or roll the work back.
It is at this point in the procedure that you would put in any logic appropriate for determining “old” passwords for your application. In the case of the sample provider, only the last 10 passwords for a user are retained. Passwords are sorted according to when the records were created, with the oldest records being candidates for deletion. When you get to the eleventh and subsequent passwords, the stored procedure automatically purges the older records. If you don’t have some type of logic like this, over time the password history tracking will get slower and slower. After the old password purge is completed the transaction is committed. For the sake of brevity, more extensive error handling is not included inside of the transaction. Theoretically, something could go wrong after the insert or delete statement, which would warrant more extensive error handling than that shown in the previous sample.
The companion to the insert stored procedure is a procedure to retrieve the current password history for a user:
create procedure dbo.GetPasswordHistory @pUserName nvarchar(256), @pApplicationName nvarchar(256) as
select |
[Password], PasswordSalt, CreateDate |
|
from |
dbo.PasswordHistory ph, |
|
|
dbo.vw_aspnet_Applications a, |
|
|
dbo.vw_aspnet_Users |
u |
where |
a.LoweredApplicationName |
= LOWER(@pApplicationName) |
and |
a.ApplicationId |
= u.ApplicationId |
and |
u.LoweredUserName |
= LOWER(@pUserName) |
and |
ph.UserId |
= u.UserId |
order by CreateDate DESC
This procedure is pretty basic — it accepts the username and application name and uses these two values to get to the UserId. At which point, the procedure returns all of the rows from the PasswordHistory table with the most recent passwords being retrieved first.
The next step in developing the custom provider is to rough out its class signature:
using System;
using System.Configuration;
using System.Configuration.Provider; using System.Data;
using System.Data.SqlClient;
using System.Security.Cryptography; using System.Text;
using System.Web.Configuration; using System.Web.Security;
public class ProviderWithPasswordHistory : SqlMembershipProvider
443

Chapter 11
{
private string connectionString;
//Overrides of public functionality
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
public override string ResetPassword(string username, string passwordAnswer)
public override MembershipUser CreateUser(...)
public override bool ChangePassword(string username, string oldPassword, string newPassword)
//Private methods that provide most of the functionality private byte[] GetRandomSaltValue()
private void InsertHistoryRow(string username, string password)
private bool PasswordUsedBefore(string username, string password)
The custom provider will perform some extra initialization logic in its Initialize method. Then the actual enforcement of password histories occurs within ChangePassword and ResetPassword. CreateUser is overridden because the very first password in the password history is the one used by the user when initially created. The private methods support functionality that uses the data layer logic you just saw: the ability to store password history as well as a way to determine whether a password has ever been used before. The GetRandomSaltValue method is used to generate random salt prior to storing password history records.
Start out looking at the Initialize method:
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
//We need the connection string later
//So grab it before the SQL provider removes it from the //configuration collection.
string connectionStringName = config[“connectionStringName”];
base.Initialize(name, config);
if (PasswordFormat != MembershipPasswordFormat.Hashed) throw new NotSupportedException(
“You can only use this provider with hashed passwords.”);
connectionString = WebConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
}
The override uses the connection string name that was configured on the provider (that is, the provider’s connectionStringName attribute) to get the connection string from the <connectionStrings />section. The provider also performs a basic sanity check to ensure that the password format has been set to use hashed passwords. If you want you can follow the same approach shown for this sample provider and extend it to support password histories for encrypted passwords.
444

SqlMembershipProvider
The first step in the lifecycle of a user is the initial creation of that user’s data in the Membership tables. Because the custom provider tracks a user’s password history, it needs to store the very first password that is created. It does this with the private InsertHistoryRow method. The first part of this private method sets up the necessary ADO.NET command for calling the insert stored procedure shown earlier:
private void InsertHistoryRow(string username, string password)
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
//Setup the command
string command = “dbo.InsertPasswordHistoryRow”; SqlCommand cmd = new SqlCommand(command, conn); cmd.CommandType = System.Data.CommandType.StoredProcedure;
//Setup the parameters
SqlParameter[] arrParams = new SqlParameter[5];
arrParams[0] = new SqlParameter(“pUserName”, SqlDbType.NVarChar, 256); arrParams[1] = new SqlParameter(“pApplicationName”,
SqlDbType.NVarChar, 256); arrParams[2] = new SqlParameter(“pPassword”, SqlDbType.NVarChar, 128);
arrParams[3] = new SqlParameter(“pPasswordSalt”, SqlDbType.NVarChar, 128); arrParams[4] = new SqlParameter(“returnValue”, SqlDbType.Int);
So far, this is all pretty standard ADO.NET coding practices. The next block of code gets interesting, though, because it is where a password is hashed with a random salt prior to storing it in the database:
//Hash the password again for storage in the history table byte[] passwordSalt = this.GetRandomSaltValue();
byte[] bytePassword = Encoding.Unicode.GetBytes(password); byte[] inputBuffer = new byte[bytePassword.Length + 16];
Buffer.BlockCopy(bytePassword, 0, inputBuffer, 0, bytePassword.Length);
Buffer.BlockCopy(passwordSalt, 0, inputBuffer, bytePassword.Length, 16);
HashAlgorithm ha = HashAlgorithm.Create(Membership.HashAlgorithmType); byte[] bhashedPassword = ha.ComputeHash(inputBuffer);
string hashedPassword = Convert.ToBase64String(bhashedPassword); string stringizedPasswordSalt = Convert.ToBase64String(passwordSalt);
As a first step, the provider gets a random 16-byte salt value as a byte[]. Because this salt value needs to be combined with the user’s password, the password is also converted to a byte[]. Then the salt value and the byte representation of the password are combined using the Buffer object into a single array of bytes that looks like: byte[password as bytes, 16 byte salt value]. This approach ensures that the hashed password will be next to impossible to reverse engineer — but it does so without relying on the internal byte array format used by SqlMembershipProvider when it hashes passwords. This means more code in the custom provider, but it also means the provider’s approach to securely storing passwords won’t break if the internal implementation of SqlMembershipProvider changes in a future release.
With the combined values in the byte array, the provider uses the hash algorithm configured for Membership to convert the array into a hashed value. At this point, both the resultant hash and the random salt that were used are converted in a base64-encoded string for storage back in the database.
445

Chapter 11
//Put the results into the command object arrParams[0].Value = username; arrParams[1].Value = this.ApplicationName; arrParams[2].Value = hashedPassword;
arrParams[3].Value = stringizedPasswordSalt; //need to remember the salt arrParams[4].Direction = ParameterDirection.ReturnValue;
cmd.Parameters.AddRange(arrParams);
//Insert the row into the password history table
conn.Open();
cmd.ExecuteNonQuery();
int procResult = (int)arrParams[4].Value; conn.Close();
if (procResult != 0)
throw new ProviderException(
“An error occurred while inserting the password history row.”);
}
}
The remainder of the InsertHistoryRow method packages up all of the data into SqlCommand object’s parameters and then inserts them using the InsertPasswordHistoryRow stored procedure. Because the stored procedure returns a -1 value if it could not find the user in the vw_aspnet _MembershipUsers view or if an error occurred during the insert, the provider checks for this error condition and throws an exception if this occurs.
Because this method relies on generating a random 16-byte salt, take a quick look at the private helper method that creates the salts:
private byte[] GetRandomSaltValue()
{
RNGCryptoServiceProvider rcsp = new RNGCryptoServiceProvider(); byte[] bSalt = new byte[16];
rcsp.GetBytes(bSalt); return bSalt;
}
This code should look familiar from the earlier topic on custom password generation. In this case, the random number generator is used to create a fixed length array of random bytes that will be used as a salt for the provider’s hashing. The use of a salt value makes it substantially more difficult for anyone to guess a password stored in the password history table using a dictionary-based attack.
The create user method looks like this:
public override MembershipUser CreateUser(
string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey,
out MembershipCreateStatus status)
{
MembershipUser mu;
mu = base.CreateUser(username, password, email,
passwordQuestion, passwordAnswer,
446

SqlMembershipProvider
isApproved, providerUserKey, out status);
if (status != MembershipCreateStatus.Success) return mu;
//Only insert the password row if the user was created try {
InsertHistoryRow(username, password); return mu;
}
catch(Exception ex)
{
//Attempt to cleanup after a creation failure base.DeleteUser(username,true);
status = MembershipCreateStatus.ProviderError; return null;
}
}
The custom provider doesn’t attempt to save the password unless the user is successfully created by SqlMembershipProvider. If the base provider is successful, then the password history is inserted with a call to the custom provider’s InsertHistoryRow method. If the call is successful (which should always be the case unless something goes wrong with the database), then the MembershipUser instance returned from the base provider is returned to the caller. If something does go wrong, the custom provider attempts to compensate by deleting the newly created user. This is intended to prevent the case where the user is created in the database, but the password is not properly logged to the password history. In the error case, the custom provider returns a ProviderError status code to indicate to the caller that the CreateUser method did not succeed.
At this point, you can test the custom provider with a page that uses the CreateUserWizard control. Configure the wizard control to use an instance of the custom provider:
In config:
<add name=”passwordHistoryProvider” type=”ProviderWithPasswordHistory” connectionStringName=”LocalSqlServer” applicationName=”passwordHistory”/>
On the page:
<asp:CreateUserWizard ID=”CreateUserWizard1” runat=”server” ...other attributes...
MembershipProvider=”passwordHistoryProvider” />
Now you can use CreateUserWizard to create new users. For each newly created user, the initial password is logged to the PasswordHistory table:
UserId |
{A71E13F5-DB58-4E10-BEB4-9825E5A263F2} |
Password |
tJUZ5K1A5JuWcrZoJjF1OMXGM+8= |
PasswordSalt |
B8sbL04yOYwGyYZHT7AADA== |
CreateDate |
2005-07-27 21:04:10.257 |
447

Chapter 11
So far so good — a user is registered in the Membership tables and the initial password is stored in the history. The next step is to get the custom provider working with the ChangePassword method. Changing a password requires the provider to retrieve the history of all of the user’s passwords and then search through the history to see if any of the old passwords match the value of the new password passed to ChangePassword.
The private method PasswordUsedBefore returns a bool value indicating whether or not a given password has ever been used before by a user. The first part of the method just uses standard ADO.NET calls to retrieve the password history using the GetPasswordHistory stored procedure:
private bool PasswordUsedBefore(string username, string password)
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
//Setup the command
string command = “dbo.GetPasswordHistory”; SqlCommand cmd = new SqlCommand(command, conn);
cmd.CommandType = System.Data.CommandType.StoredProcedure;
//Setup the parameters
SqlParameter[] arrParams = new SqlParameter[2];
arrParams[0] = new SqlParameter(“pUserName”, SqlDbType.NVarChar, 256); arrParams[1] = new SqlParameter(“pApplicationName”,
SqlDbType.NVarChar, 256);
arrParams[0].Value = username; arrParams[1].Value = this.ApplicationName;
cmd.Parameters.AddRange(arrParams);
//Fetch the password history from the database DataSet dsOldPasswords = new DataSet(); SqlDataAdapter da = new SqlDataAdapter(cmd); da.Fill(dsOldPasswords);
The end result of this code is a DataSet and a DataTable containing one or more rows of old passwords for the user from the PasswordHistory table. The interesting part of the method involves comparing each row of old password data in the returned DataSet to the password parameter that was passed to the method.
HashAlgorithm ha = HashAlgorithm.Create(Membership.HashAlgorithmType); foreach (DataRow dr in dsOldPasswords.Tables[0].Rows)
{
string oldEncodedPassword = (string)dr[0]; string oldEncodedSalt = (string)dr[1];
byte[] oldSalt = Convert.FromBase64String(oldEncodedSalt);
byte[] bytePassword = Encoding.Unicode.GetBytes(password); byte[] inputBuffer = new byte[bytePassword.Length + 16];
Buffer.BlockCopy(bytePassword, 0, inputBuffer, 0, bytePassword.Length); Buffer.BlockCopy(oldSalt, 0, inputBuffer, bytePassword.Length, 16);
byte[] bhashedPassword = ha.ComputeHash(inputBuffer);
448

SqlMembershipProvider
string hashedPassword = Convert.ToBase64String(bhashedPassword);
if (hashedPassword == oldEncodedPassword) return true;
}
}
//No matching passwords were found if you make it this far return false;
}
Once again, an instance of HashAlgorithm matching hashAlgorithmType for the Membership feature is used. Each row of password data from the database has the password salt that was used to hash and encode the result that is stored in the corresponding Password column. Much like the original hashing done inside of InsertHistoryRow, the PasswordUsedBefore method converts the password parameter into a byte array and combines it with the byte array representation of the password salt retrieved from the database. This combination is then hashed using the hashing algorithm created a few lines earlier in the code.
To make it easier to compare the hashed value of the password parameter to the old password from the database, the result of hashing the password parameter with the old salt value is converted to a base64encoded string. As a result, the comparison is as simple as comparing the string from the database (that is, the Password column) to the base64-encoded representation of the encoded password parameter. If the two strings match, the method knows that the password parameter has been used before for that user, and the method returns true. If the method loops through all of the password history records in the database and never finds a match, the method returns false, indicating that the password parameter has never been used before.
One thing to note about the password history implementation is that each old password is encoded using a different random salt value. That is why for each row of password history data retrieved from the database the custom provider must rehash the password parameter for comparison. A second thing to note about the implementation of the PasswordUsedBefore method is that it does not include any protections against two different threads of execution both attempting to change the password for the same user. It is theoretically possible that on two different web servers (or two different threads on the same server) a change password operation could be occurring at the same time.
However, if this occurs one of two things happens. Both operations could be attempting to change the user’s password to the same value in which case one of the two password change operations would effectively end up as a no-op — but the same password would show up twice in the password history table. In the alternative outcome, one change password successfully completes before the other change password attempt — in which case the second password change attempt would fail because it would be using the wrong value for the oldPassword parameter. The net outcome though is that this scenario has a low likelihood of occurring, and even if it does occur it has little effect on the overall security and accuracy of the password history feature.
Now that you have seen how the custom provider can compare a new password against all of the old passwords in the database, look at how it is used from the ChangePassword method:
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
if (PasswordUsedBefore(username, newPassword))
449

Chapter 11
return false;
bool result = base.ChangePassword(username, oldPassword, newPassword);
if (result == false) return result;
//Only insert the password row if the password was changed try
{
InsertHistoryRow(username, newPassword); return true;
}
catch (Exception ex)
{
//Attempt to cleanup after a failure to log the new password base.ChangePassword(username, newPassword, oldPassword); return false;
}
}
First, the ChangePassword override validates the newPassword parameter against the password history. If the newPassword parameter matches any of the old passwords, then the method immediately returns false. Remember that because ChangePassword returns a bool, the convention used by the Membership feature is to return a false value as opposed to throwing an exception.
If no old matching passwords were found, the provider calls into the base provider to perform the password change operation. If for some reason the base provider fails, a false is also returned. If the base provider succeeds, though, the custom provider needs to store the new password in the password history table with a call to InsertHistoryRow. Normally, this operation succeeds, and the caller receives a true return value indicating that the password was successfully changed.
If the password history was not successfully updated, the custom provider compensates for the failure by resetting the user’s password to the original value. If you look at the call to the base provider in the catch block you can see that the two password parameters from the original method call are simply reversed to cause the user to revert to the original password. And, of course, in the failure case a false value is again returned to the caller.
You can try the password change functionality with a simple page using the ChangePassword Login control configured to use the custom provider.
<asp:ChangePassword ID=”ChangePassword1” runat=”server” MembershipProvider=”passwordHistoryProvider” />
After logging in with an account created using the custom provider, you can navigate to the change password page and try different variations of new passwords. For each new unique password another new row shows up in the PasswordHistory table. However, for each new non-unique password the ChangePassword control displays an error message saying the new password is invalid. Although I won’t show it here, you can easily write some code that integrates between the custom provider’s behavior and the ChangePassword control that would allow error messages to be more precise whenever duplicate passwords are used.
450

SqlMembershipProvider
The last piece of functionality that the custom provider implements is the ResetPassword method:
public override string ResetPassword(string username, string passwordAnswer)
{
string newPassword = base.ResetPassword(username, passwordAnswer);
//No recovery logic at this point InsertHistoryRow(username, newPassword);
return newPassword;
}
The custom provider delegates to the base provider to reset the password. There isn’t any need to compare the reset password against the password history because the default reset password logic generates a completely random new password. Unless you are worried about the one in a billion chance (or so) of repeating a random password, you can save yourself the performance hit of checking against the password history for this case. If the password reset succeeds, the override calls InsertHistoryRow to store the auto-generated password in the PasswordHistory table.
Unlike CreateUser and ChangePassword, the sample code does not attempt to recover from a problem at this point. A simple try-catch block can’t compensate for errors in the case of resetting passwords. You could use the new ADO.NET 2.0 TransactionScope class though to wrap both the base provider SQL calls and the password history SQL code in a single transaction. This approach would also be a more elegant solution to the compensation logic shown earlier for the CreateUser and
ChangePassword overloads.
Account Lockouts
Membership providers can choose to implement account lockouts as a protection against brute force guessing attacks against a user’s password and password answer. SqlMembershipProvider implements protections against both attacks and will lock out accounts for both cases. Deciphering the provider configuration attributes for account lockouts and trying to understand exactly when accounts are locked in SQL can be a bit confusing when using the SQL provider.
SqlMembershipProvider keeps track of failed attempts at using a password by storing tracking information in the FailedPasswordAttemptCount and FailedPasswordAttemptWindowStart columns of the aspnet_Memership table. The provider tracks failed attempts at using a password answer separately in a different set of columns: FailedPasswordAnswerAttemptCount and FailedPasswordAnswerAttemptWindowStart. When a user is first created the counter columns are set to a default value of zero while the date-time columns are set to default values of 01/01/1754.
Each time a provider method is called that accepts a password parameter, the provider internally validates that the password is correct. ValidateUser is the most common method where this occurs, but password validation also occurs for ChangePassword (validating the old password) as well as ChangePasswordQuestionAndAnswer. The first time an incorrect password is supplied, two things occur:
451