Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
51
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 11

But even this doesn’t solve the problem because as part of the logic inside of ChangePassword, the provider first fetches the existing password information, including the password format from the database. The provider internally validates the oldPassword parameter of this method using the password data and format retrieved from the database. Assuming that this validation succeeds the provider encodes the newPassword parameter using the password format that is stored in the database. As a result, there isn’t a way to get in between the validation of the oldPassword and the encoding of newPassword parameter to tell the provider to use a new password format.

For this reason, you should avoid situations that require changing the password format for a production system. If you try to change a production system from using hashed passwords to using encrypted passwords, you really don’t have any option other than recreating user accounts on the fly when users log in. With hashed passwords, you can’t automate the change, because there is no way to get back to the cleartext versions of the passwords.

If you try to change a production system from using encrypted passwords to using hashed passwords, you can potentially automate this because you at least know the decryption key. However, you will need to write code that converts from the base64-encoded representations of the password and password answers into a byte[], at which point you have to write your own code to decrypt the passwords using the correct algorithm. This method comes with a potential privacy issue because your website customers probably don’t expect to have their passwords decrypted for any reason other than logging in.

As you can see, neither of these scenarios are optimal — so make sure that the password format you plan to use is determined well before your website goes into production. After you have live users on your site, changing your mind about the password format can require you to delete and then regenerate existing user accounts.

Custom Password Generation

If you use the password reset feature of SqlMembershipProvider, then you will be depending on the default behavior the provider supplies for automatically generating passwords. The default behavior uses the Membership.GeneratePassword method to create a password that conforms to the configured password strength requirements. These are defined by the provider’s minRequiredPasswordLength and minRequiredNonAlphanumericCharacters configuration attributes. Note that even if you set the minRequiredNonAlphanumericCharacters attribute to zero, it is likely that the auto-generated password will still contain nonalphanumeric characters.

The internal implementation of Membership.GeneratePassword randomly selects password characters from a predefined set of nonalphanumeric characters as well as the standard set of uppercase

and lowercase alphanumeric characters and numbers. As a result the GeneratePassword method only guarantees that there are at least as many nonalphanumeric characters as required by the minRequiredNonAlphanumericCharacters. The method does not guarantee creating exactly as many nonalphanumeric characters as specified in the configuration attribute; instead, it is likely that GeneratePassword will generate a few more nonalphanumeric characters than specified by minRequiredNonAlphanumericCharacters.

If you don’t want this behavior, or if you have your own requirements and algorithm for creating random passwords, you can choose to override the public virtual GeneratePassword method defined on

SqlMembershipProvider.

public virtual string GeneratePassword();

432

SqlMembershipProvider

An override of this virtual method doesn’t take any parameters and is expected to return a string containing the randomly generated password. You have access to the provider’s configured password strength requirements via MinRequiredPasswordLength and MinRequiredNonAlphanumericCharacters that are defined up on MembershipProvider.

As an example of this, you can write a provider that derives from SqlMembershipProvider and that overrides just the GeneratePassword method. For simplicity, you can implement the derived provider in the App_Code directory of your website; although if you needed this functionality available across all of your websites you would instead create a derived provider using a standalone class library.

The following sample code shows a custom password generator that handles the case where zero nonalphanumeric characters are required:

using System;

using System.Web.Security;

using System.Security.Cryptography;

public class CustomPasswordGeneration : SqlMembershipProvider

{

private static char[] randChars = “a0bcde1fghij2klmno3pqrst4uvwxy5zABCD6EFGHI7JKLMN8OPQRS9TUVWXYZ”.ToCharArray();

public override string GeneratePassword()

{

if (MinRequiredNonAlphanumericCharacters == 0)

{

RNGCryptoServiceProvider rcsp = new RNGCryptoServiceProvider(); //Always generate at least 14 characters in the random password int desiredLength =

MinRequiredPasswordLength < 14 ? 14 : MinRequiredPasswordLength;

byte[] randBytes = new byte[desiredLength]; char[] convertedResult = new char[desiredLength];

//First get some random values rcsp.GetBytes(randBytes);

//Then convert these values into characters for (int i = 0; i < desiredLength; i++)

{

int indexOffset = ((int)randBytes[i]) % randChars.Length; convertedResult[i] = randChars[indexOffset];

}

return new String(convertedResult);

}

else

{

return base.GeneratePassword();

}

}

}

433

Chapter 11

The sample code overrides just the GeneratePassword method of SqlMembershipProvider. In the event that the custom provider is configured to not require nonalphanumeric characters, then the custom password generation logic runs. Otherwise, the override just delegates to the base class. You can of course extend this to handle cases that require nonzero number of nonalphanumeric characters, and you want to specify the exact number of nonalphanumeric characters allowed.

The custom password generator follows the same approach as the default Membership providers by always generating at least a 14-character long random password. In the unlikely event that the provider is configured to require even more characters, it will honor the longer length instead. The custom provider first gets the appropriate number of random byte values using RNGCryptoServiceProvider. This ensures that the values are truly random as opposed to having some hidden dependency on a known seed.

The byte values are then converted into characters by treating each random byte value as an integer and then performing a modulus operation on the integer. The resulting value is used as an index into the fixed character array randChars defined at the start of the class. The custom provider implementation allows only uppercase and lowercase representations of a–z as well as the numbers 0–9 in a randomly generated password. Using this approach you can easily change the characters allowed in a random password by editing the characters in the randChars variable. Because the modulus operation always runs based on the length of randChars, you can change the length of the array without worrying about updating constants elsewhere in the code.

After each random byte has been converted into a character, the array of characters is returned as a string. You can try this code out with the sample configuration shown here:

<add name=”customPasswordGeneration” type=”CustomPasswordGeneration” connectionStringName=”LocalSqlServer” minRequiredNonalphanumericCharacters=”0”

/>

Notice that the type string for the provider contains only the name of the class. This works because the ASP.NET ProvidersHelper class that you saw earlier in Chapter 9 has extra logic that can resolve types from special ASP.NET directories, including the App_Code directory. As a result, the assembly name and optional string name information is not required for this case.

If you run a sample page with code like the following:

CustomPasswordGeneration cgprovider = (CustomPasswordGeneration)Membership.Providers[“customPasswordGeneration”];

Response.Write(cgprovider.GeneratePassword());

you will get random passwords output like the following strings:

E73iDeRIs68USd

Ws25gpbZU6P2wo

U5EcY4WxissPfY

and so on.

434

SqlMembershipProvider

If you change the configuration for the custom provider to require one or more nonalphanumeric characters, the random password generation reverts to the default behavior implemented by

SqlMembershipProvider.

Implementing Custom Encr yption

In the previous chapter, you saw how to implement custom hash algorithms that work with SqlMembershipProvider. Unlike hash operations, encryption is not something that can be declaratively customized using the <membership /> element. While hash operations are pretty straightforward from an API standpoint (a byte[] goes in, and a different byte[] comes out the other side), encryption operations are not as simple to make universally configurable.

If you choose encrypted passwords with Membership, by default SqlMembershipProvider will use the encryption routines buried within the internals of the <machineKey /> configuration section. There had been consideration at one point of making the encryption capabilities in this configuration section more generic and more customizable. However, that work was never done because configuring encryption algorithms can involve quite a number of initialization parameters (initialization vectors, padding modes, algorithm specific configuration properties, and so on).

Therefore, if you want to use a custom encryption algorithm in conjunction with SqlMembershipProvider, you will need to write some code. The base class MembershipProvider exposes the EncryptPassword and DecryptPassword methods as protected virtual. You can derive from SqlMembershipProvider and override these two methods because internally the SQL provider encrypts and decrypts data by calling these base class methods. The method signatures for encryption and decryption are very basic:

protected virtual byte[] DecryptPassword( byte[] encodedPassword )

protected virtual byte[] EncryptPassword( byte[] password )

Your custom encryption implementation needs to take a byte[], either encrypt or decrypt it, and then return the output as a different byte[]. By the time decryption override is called, MembershipProvider has already converted the base64-encoded representation of the password in the database back into a byte[]. Similarly, after your custom encryption routine runs, the provider will convert the resulting byte[] back into a bas64-encoded string for storage in the database.

Remember that SqlMembershipProvider stores passwords and password answers as an nvarchar(128). Custom encryption routines that cause excessive bloat need to keep this mind. If you suspect that a custom encryption algorithm may increase the size of the password and password answer (taking into account the subsequent base64 encoding as well), you should have extra maximum length rules to prevent this problem. For passwords, you could make sure to hook the ValidatingPassword event or override password related methods on the provider to enforce a maximum password length. For password answer maximum length enforcement you always need to derive from SqlMembershipProvider because this is the only way to validate password answer lengths prior to their encoding.

SqlMembershipProvider gives some protection against excessively long encoded values because it always validates that the encoded (that is, base64 encoded) representation of passwords and password answers are less than or equal to 128 characters. If an encoded representation exceeds this length, the provider throws an exception to that effect. However, proactively checking the maximum lengths of the

435

Chapter 11

cleartext password and password answer representations makes it easier to communicate to users to limit the size of these strings. Having some kind of a client-side validation check on the browser for such lengths means that users won’t be scratching their heads wondering why a perfectly valid password or password answers keeps failing.

As a simple example for implementing custom encryption, the following code shows a custom provider that has overridden the encryption and decryption methods to instead preserve the cleartext representations of the passwords and password answers:

using System;

using System.Web.Security;

//Just replays the password/answer

public class CustomEncryption : SqlMembershipProvider

{

protected override byte[] EncryptPassword(byte[] password)

{return password; }

protected override byte[] DecryptPassword(byte[] encodedPassword) { return encodedPassword; }

}

Obviously, you would never use this kind of code in production — but the sample does make it clear how simple it is from an implementation perspective to clip in your own custom encryption and decryption logic. Assuming that you are using a commercial implementation of an encryption algorithm, the byte[] parameters to the two methods are what you would use with the System.Security.Cryptography

.CryptoStream’s Read and Write methods.

To use this custom provider, configure a sample application with a reference to the provider, making sure that you explicitly set the passwordFormat attribute for the provider.

<add name=”customEncryptionProvider” type=”CustomEncryption” passwordFormat=”Encrypted” connectionStringName=”LocalSqlServer” />

Now if you create a user with the following lines of code:

CustomEncryption cencprovider = (CustomEncryption)Membership.Providers[“customEncryptionProvider”];

MembershipCreateStatus status;

cencprovider.CreateUser(“customEncryption1”, “this is the cleartext password”, “foo@nowhere.org”, “question”,

“this is the cleartext answer”, true, null, out status);

the database contains the base64-encoded representations stored for the password and the password answer, which are really just 16-byte salt values plus the cleartext strings preserved by the custom encryption routine. It turns out that when SqlMembershipProvider encrypts passwords and password answers, it still prepends a 16-byte random salt value to the byte representation of these strings (that is, password --> unicode byte[16 byte salt, then the byte representation of the password or answer]). However, I would not recommend taking advantage of this because the existence of the salt

436

SqlMembershipProvider

value, even in encrypted passwords and password answers, is an internal implementation detail. The existence of this value as well as its location could change unexpectedly in future releases. For example, the password is stored as:

we0UiiaUuwqIdS1dS0M5/nQAaABpAHMAIABpAHMAIAB0AGgAZQAgAGMAbABlAGEAcgB0AGUAeAB0ACAAcAB

hAHMAcwB3AG8AcgBkAA==

If you convert this to a string with the following code:

string result = “base 64 string here”;

byte[] bResult = Convert.FromBase64String(result); Response.Write(Encoding.Unicode.GetString((Convert.FromBase64String(result))));

the result consists of eight nonsense characters (for the 16-byte random salt value) plus the original password string of “this is the cleartext password”. The size of the base64-encoded password representation demonstrates the bloating effect the encoding has on the password. In this case, the original password contained 30 characters; adding the random salt value results in a 38-character password. Each character consumes 2 bytes when converted in a byte array, which results in a byte[76]. However, the base64-encoded representation contains 104 characters for these 76 byte values, which is around 1.37 encoded characters for each byte value and roughly 2.7 base64 characters for each original character in the password.

If you use the default of AES encryption with SqlMembershipProvider, the same password results in 108 encoded characters — roughly the same overhead. This tells you that most of the string bloat comes from the conversion of the Unicode password string into a byte array as well as the overhead from the base64 encoding — the actual encryption algorithm adds only a small amount to the overall size. As a general rule of thumb when using encryption with SqlMembershipProvider, you should plan on three encoded characters being stored in the database for each character in the original password and password answer strings.

This gives you a safe upper limit of around 42 characters for both of these values when using encryption. For passwords, this is actually enormous because most human beings (geniuses and savants excluded!) can’t remember a 42-character long password. For password answers, 42 characters should be sufficient when using encryption as long as the password questions are such that they result in reasonable answers. Questions like what is your favorite car or color or mother’s maiden name? probably don’t result in 40+- character long answers. However, if you allow freeform password questions where the user supplies the question, the resulting answer could be excessively long. Remember, though, that even with password answers, the user has to remember the exact password answer to retrieve or reset a password. As a result, it is unlikely that a website user will create an excessively long answer, because just as with passwords, folks will have trouble remembering excessively long answers.

Enforcing Custom Password Strength Rules

By default, SqlMembershipProvider enforces password strength using a combination of the minRequiredPasswordLength, minRequiredNonalphanumericCharacters, and passwordStrengthRegularExpression provider configuration attributes. The default provider configuration in machine.config causes the provider to require at least seven characters in the password with at least one of these being a nonalphanumeric character. There is no default password strength regular expression defined in machine.config.

437

Chapter 11

If you choose to define a regular expression, the provider enforces all three password constraints: minimum length, minimum number of nonalphanumeric characters, and matching the password against the configured regular expression. If you want the regular expression to be the exclusive determinant of password strength, you can set the minRequiredPasswordLength attribute to one and the minRequiredNonalphanumericCharacters to zero. Although the provider still enforces password strength with these requirements, your regular expression will expect that passwords have at least one character in them — so effectively only your regular expression will really be enforcing any kind of substantive rules.

You can see that just with the provider configuration attributes you can actually enforce a pretty robust password. However, for security-conscious organizations password strength alone isn’t sufficient. The classic problem of course is with users and customers “changing” their passwords by simply using an old password, or by creating a new password that revs one digit or character from the old password. If you have more extensive password strength requirements, you can enforce them in one of two ways:

Hook the ValidatingPassword event on the provider — This approach doesn’t require you to derive from the SQL provider and as a result doesn’t require deployment of a custom provider along with the related configuration changes in web.config. However, you do need some way to hook up your custom event handler to the provider in every web application that requires custom enforcement.

Derive from SqlMembershipProvider and override those methods that deal with creating or changing passwords (CreateUser, ChangePassword and ResetPassword) — You have to ensure that your custom provider is deployed in such a way that each website can access it, and you also need to configure websites to use the custom provider. Because you would be overriding methods anyway, this approach also has the minor advantage of having easy access to other parameters passed to the overridden methods. With this approach, you won’t have to worry about hooking up the ValidatingPassword event.

Realistically, either approach is perfectly acceptable. The event handler was added in the first place because much of the extensibility model in ASP.NET supports event mechanisms and method overrides. For example, when you author a page, you are usually hooking events on the page and its contained controls as opposed to overriding methods like OnClick or OnLoad. For developers who have simple password strength requirements for one or a small number of sites, using the ValidatingPassword event is the easier approach.

Using the ValidatingPassword event is as simple as hooking the event on an instance of SqlMembershipProvider. To hook the event for the default provider, you can subscribe to Membership.ValdatingPassword. To hook the event on one of the nondefault provider instances, you need to first get a reference on the provider instance and then subscribe to MembershipProvider.ValidatingPassword. When the event is fired, it passes some information to its subscribers with an instance of ValidatingPasswordEventArgs.

public sealed class ValidatePasswordEventArgs : EventArgs

{

public ValidatePasswordEventArgs( string userName,

string password, bool isNewUser )

public string UserName { get; }

438

SqlMembershipProvider

public string Password { get; } public bool IsNewUser { get; } public bool Cancel {get; set; }

public Exception FailureInformation {get; set;}

}

An event handler knows the user that the password creation or change applies to from the UserName property. You know whether the password in the Password parameter is for a new password (that is, CreateUser was called) or a changed password (that is, ResetPassword or ChangePassword was called) by looking at the IsNewUser property. If the property is true, then the UserName and Password are for a new user — otherwise, the event represents information for an existing user who is changing or resetting a password. The event handler doesn’t know the difference between a password change and a password reset.

After an event handler has inspected the password using whatever logic it wants to apply, it can indicate the success of failure of the check via the Cancel property. If the custom password strength validation fails, then the event handler must set this property to true. If you also want to return some kind of custom exception information, you can optionally new() up a custom exception type and set it on the FailureInformation property. Remember that SqlMembershipProvider always returns a status code of MembershipCreateStatus.InvalidPassword from CreateUser. As a result of this method’s signature, the provider doesn’t throw an exception when password strength validation fails — instead it just returns a failure status code.

SqlMemershipProvider will throw an exception if a failure occurs in either ChangePassword or ResetPassword. It will throw the custom exception from FailureInformation if it is available. If an event handler only sets Cancel to true, the provider throws ArgumentException from

ChangePassword or ProviderException from ResetPassword. Remember that if you want to play well with the Login controls, the exception type that you set on FailureInformation should derive from one of these two exception types.

The reason for the different exception types thrown by SqlMembershipProvider is that in ChangePassword, the new password being validated is something your user entered, and hence ArgumentException is appropriate. In the case of ResetPassword though, the new password is automatically generated with a call to GeneratePassword. Because the new password is not something supplied by user input, throwing ArgumentException seemed a bit odd. So instead, ProviderException is thrown because the provider’s password generation code failed. Unless you use password regular expressions, you probably won’t run into ProviderException being thrown from ResetPassword. Because you can’t determine if you are being called from ChangePassword or ResetPassword from inside of the ValidatingPassword event, it is reasonable to throw either exception type.

Hooking the ValidatePassword Event

When you hook the ValidatingPassword event, SqlMembershipProvider will raise it from inside of CreateUser, ChangePassword, and ResetPassword. The simplest way to perform the event hookup is from inside global.asax, with the actual event existing in a class file in the App_Code directory.

A custom event handler needs to have the same signature as the event definition:

public delegate void MembershipValidatePasswordEventHandler(

Object sender, ValidatePasswordEventArgs e );

439

Chapter 11

The following sample code shows a password strength event handler that enforces a maximum length of 20 characters for a password. If the length is exceeded, it sets an ArgumentException on the event argument:

public class ValidatingPasswordEventHook

{

public static void LimitMaxLength(Object s, ValidatePasswordEventArgs e)

{

if (e.Password.Length > 20)

{

e.Cancel = true; ArgumentException ae =

new ArgumentException(“The password length cannot exceed 20 characters.”); e.FailureInformation = ae;

}

}

}

The event handler is written as a static method on the ValidatingPasswordEventHook class. Because the event may be called at any time within the life of an application, it makes sense to define the event handler using a static method so that it is always available and doesn’t rely on some other class instance that was previously instantiated.

The sample event handler is hooked up inside of global.asax using the Application_Start event:

void Application_Start(object sender, EventArgs e)

{

SqlMembershipProvider smp = (SqlMembershipProvider)Membership.Providers[“sqlPasswordStrength”];

smp.ValidatingPassword +=

new MembershipValidatePasswordEventHandler( ValidatingPasswordEventHook.LimitMaxLength);

}

In this case, the event hookup is made using a provider reference directly as opposed to hooking up to the default provider via the Membership.ValidatingPassword event property. Now if you attempt to create a new user with an excessively long password, you receive InvalidStatus as the output parameter. For existing users, if you attempt to change the password with an excessively long password, ArgumentException set inside of the event handler is thrown instead.

Implementing Password History

A more advanced use of password strength validation is enforcing the rule that previously used passwords not be reused for new passwords. Although SqlMembershipProvider doesn’t expose this kind of functionality, you can write a derived provider that keeps track of old passwords and ensures that new passwords are not duplicates. The sample provider detailed in this section keeps track of password history when hashed passwords are used. Hashed passwords are used for this sample because it is a somewhat more difficult scenario to handle.

Neither SqlMembershipProvider nor the base MembershipProvider class expose the password salts for hashed passwords. Without this password salt, you need to do some extra work to keep track of password history in a way that doesn’t rely on any hacks or undocumented provider behavior. The

440

SqlMembershipProvider

remainder of this section walks you through an example that extends SqlMembershipProvider by incorporating password history tracking. The sample provider checks new passwords against the history whenever ChangePassword is called. It adds items to the password history when a user is first created with CreateUser, and whenever the password subsequently changes with ChangePassword or

ResetPassword.

As a first step, the custom provider needs a schema for storing the password history:

create table dbo.PasswordHistory (

 

UserId

uniqueidentifier

NOT NULL,

Passwordvarchar(128)

NOT NULL,

PasswordSalt

nvarchar(128)

NOT NULL,

CreateDate

datetime

NOT NULL

)

 

 

alter table dbo.PasswordHistory add constraint PKPasswordHistory PRIMARY KEY (UserId, CreateDate)

alter table dbo.PasswordHistory add constraint FK1PasswordHistory FOREIGN KEY (UserId) references dbo.aspnet_Users(UserId)

The provider stores one row for each password that has been associated with a user. It indexes the history on a combination of the UserId as well as the UTC date-time that the password was submitted to the Membership system. This allows each user to have multiple passwords, and thus multiple entries in the history. The table also has a foreign key pointing to the aspnet_Users table just to ensure that the user really exists and that if the user is eventually deleted that the password history rows have to be cleaned up as well. As noted earlier in the chapter, this foreign key relationship is not officially supported because it is directly referencing the aspnet_Users table. However, this is the only part of the custom provider that uses any Membership feature that is considered undocumented.

As you can probably infer from the column names, the intent of the table is to store an encoded password representation and the password salt that was used to encode the password. Because the custom provider that uses this table supports hashing, each time a new password history record is generated the custom provider needs to store the password in a secure manner. It does this by hashing the password with the same algorithm used to hash the user’s login password. Just like SqlMembershipProvider, the custom provider will actually hash a combination of the user’s password and a random salt value to make it much more difficult for someone to reverse engineer the hash value stored in the Password column. Because of this, the table also has a column where the random salt value is stored — though this salt value isn’t the same salt the provider uses for hashing the user’s login password.

Whenever a password history row has to be inserted, the following stored procedure will be used:

create procedure dbo.InsertPasswordHistoryRow @pUserName nvarchar(256), @pApplicationName nvarchar(256), @pPassword nvarchar(128), @pPasswordSalt nvarchar(128)

as

declare @UserId uniqueidentifier select @UserId = UserId

from dbo.vw_aspnet_Applications a,

441