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

Asp Net 2.0 Security Membership And Role Management

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

Chapter 11

The FailedPasswordAttemptCount in the database is incremented by one.

The FailedPasswordAttemptWindowStart column is set to the current UTC date-time.

The next time a method that accepts a password parameter is called, the provider realizes that a bad password was supplied sometime in the past. Therefore, the provider configuration attributes passwordAttemptWindow and maxInvalidPasswordAttempts are used.

Assume that a method call is made that requires a password, and that on the second attempt a bad password again is used. The provider needs to determine whether or not this second bad attempt is a discrete event, or if it should be considered part of a continuing chain of correlated password attempts. To make this determination, the provider compares the value of [ (current UTC date-time) – “passwordAttemptWindow” ] against the FailedPasswordAttemptWindowStart value in the database. If the current bad password attempt has occurred within passwordAttemptWindow minutes from FailedPasswordAttemptWindowStart, then the provider considers the current bad attempt to be related to previous bad password attempts, and the provider increments FailedPasswordAttemptCount. The provider also updates FailedPasswordAttemptWindowStart to the current UTC date-time.

For example, if the data indicates a bad password was supplied at 10:00 AM UTC, and the passwordAttemptWindow is set to 10 (that is, 10 minutes), a subsequent bad password attempt that occurs anywhere from 10:00AM UTC through 10:10 AM UTC is considered related to the original bad password attempt. As a result, the bad password attempt counter will be incremented by one, and the window start will be updated to the current date-time. This last operation is very important to note. You might think that a passwordAttemptWindow setting of 10 minutes means that all bad passwords within a fixed 10 minute period are counted. However, this is not how the SQL provider works.

Instead, the tracking window is always rolled forward whenever a bad password attempt occurs within passwordAttemptWindow minutes from the last bad password attempt. The reason for this behavior is that if the provider only tracked bad password attempts in a fixed window you could end up with the following sequence of events (assume a lockout on the fifth bad attempt and a 10-minute tracking window):

Bad password attempt #1 at 10:00 AM UTC

Bad password attempt #2 at 10:08 AM UTC

Bad password attempt #3 at 10:09 AM UTC

Bad password attempt #4 at 10:10 AM UTC

Bad password attempt #1 at 10:11 AM UTC <-- what happens here?

Bad password attempt #2 at 10:12 AM UTC

Bad password attempt #3 at 10:13 AM UTC

Bad password attempt #4 at 10:14 AM UTC

If the provider started a fixed tracking window at 10:00 AM UTC in this example and started counting, it would eventually count four bad attempts by 10:10 AM UTC. But when the next bad password attempt occurs at 10:11 AM UTC, the provider would throw away all of the old attempts because the first 10minute tracking window had expired. You could now continue to rack up more bad password attempts starting at 10:11 AM UTC. In the example, you could have four more bad password attempts starting at 10:11 AM UTC with no ill effect. The problem with this behavior is that if you look backward in time, you see that from 10:08 AM UTC through 10:14 AM UTC there have been seven bad password attempts in a 10-minute period, and yet the provider did not trigger an account lockout.

Of course, this is only a theoretical example because SqlMembershipProvider instead rolls the start of the tracking time window forward with each bad attempt. If you step through the same sequence of events with the SQL provider you instead have the following behavior:

452

 

SqlMembershipProvider

FailedPasswordAttemptWindowStart

Bad password attempt #1 at 10:00 AM UTC

10:00 AM UTC

Bad password attempt #2 at 10:08 AM UTC

10:08 AM UTC

Bad password attempt #3 at 10:09 AM UTC

10:09 AM UTC

Bad password attempt #4 at 10:10 AM UTC

10:10 AM UTC

Bad password attempt #5 at 10:11 AM UTC lockout!

10:11 AM UTC

Cannot login due to lockout at 10:11 AM UTC

 

Cannot login due to lockout at 10:11 AM UTC

 

Cannot login due to lockout at 10:11 AM UTC

 

etc...

 

In this case, with each bad password attempt the provider looks back in time to determine whether or not the current attempt is correlated to the last bad attempt as stored in the FailedPasswordAttemptWindowStart column. Because each of the first five attempts all occur less than 10 minutes apart, each attempt causes the bad password attempt counter to increment and the start of the tracking window is updated as well. As a result, when the fifth attempt occurs at 10:11AM UTC, the provider increments the counter and realizes that the maxInvalidPasswordAttempts threshold has been hit. As a result the provider locks the account out at this point. Any subsequent password attempts never make it far enough to attempt validating the password because the provider sees that the account has already been locked out.

Note that SqlMemershipProvider interprets the maxInvalidPasswordAttempts configuration attribute as a trip wire. If the number of bad password attempts exactly matches the value of this configuration setting the account is immediately locked out. So, technically speaking a setting of 5 really means a user is allowed only four bad passwords — the fifth incorrect password results in a lockout. If you happen to write a custom provider you can certainly choose to interpret this configuration attribute differently — for example a custom provider could choose to only lock out the user on the sixth attempt, in which case the attribute would be considered a threshold rather than a limit that triggers a lockout.

The previous discussion focused on bad password attempts — the exact same logic applies though to bad password answer attempts. Any methods that accept a password answer (ResetPassword and GetPassword) cause the provider to keep track of bad answer attempts using the exact same logic and the exact same provider configuration attributes. The only difference is that the counter and window start information is stored in a separate set of columns than the tracking information for bad passwords.

This raises an interesting question: What happens if a user enters bad passwords and bad password answers for an account? Until the limit specified by maxInvalidPasswordAttempts is reached the provider increments counters and updates the start windows using different columns in the database. For a time this means that bad password attempts and bad password answer attempts are considered separate occurrences that have no effect on each other. Assume that the bad password and bad password answer counters both reach 4 (the default for maxInvalidPasswordAttempts in machine.config is 5).

The next bad attempt that occurs (either password or password answer) within the tracking time window will trigger an account lockout. So even though bad attempts for passwords and answers have been tracked independently up to this point, after one of the counters hits the tripwire defined by maxInvalidPasswordAttempts, the user is locked out. A locked-out user account is no longer allowed to validate passwords with the provider and a locked out user account can no longer use the password- answer-related methods. An account lockout triggered by one type of bad information locks everything out. The provider doesn’t lock out only password-related functionality, only answer-related functionality.

453

Chapter 11

Of course, after a user account is locked out, you need some way to unlock the account. The SQL provider does not incorporate the concept of automatic account lockouts (more on this in the next section). However, the AD-based provider does support automatic unlocking because the Active Directory engine natively has this functionality. For the SQL provider, you need to explicitly call the UnlockUser method to unlock user accounts. When UnlockUser is called the following occurs:

1.The user account is unlocked: IsLockedOut is reset to false.

2.The password counter in the database is reset to zero, and the password window start column is reset to 01/01/1754.

3.The password answer counter in the database is reset to zero, and the password window start column is reset to 01/01/1754.

This behavior means that when you inspect a MembershipUser object, the LastLockoutDate property contains a useful value only when IsLockedOut is set to true. When a user account is not locked out the LastLockoutDate property contains a bogus default value. Furthermore, the MembershipUser object does not indicate what caused the lockout (was it bad passwords or bad password answers?). It only indicates that a lockout has occurred. If you need to determine the specific reason for the lockout, you can query the vw_aspnet_MembershipUsers view because the view exposes the four columns that store the passwordand password-answer-tracking information.

The tracking information is also reset during the normal course of calling provider methods with valid passwords and valid password answers. The automatic reset of the tracking information occurs in the following ways:

When a valid password is used for ValidateUser, ChangePassword or ChangePasswordQuestionAndAnswer both the passwordand password-answer-tracking columns are reset to their defaults (that is, zero and 01/01/1754).

When a valid password answer is used for ResetPassword or GetPassword only, the password answer tracking columns are reset to their defaults.

All tracking information is reset when a good password is supplied because the password is considered the main source of security for a user account. If a user supplies a correct password, that is considered proof that at a specific point in time the user knows the “master” credential for the account. As a result, the password answer counters are also reset because the password answer is considered a “secondary” credential for the account. However, if a correct password answer is supplied to a method, that is only considered good enough to reset the answer-related-tracking counters. Knowing the password answer is not considered sufficient proof that a user also knows the “master” credential for the account.

Implementing Automatic Unlocking

One potential issue that folks raise about SqlMembershipProvider is that the current lockout behavior can lead to a denial of service (DoS) attack. Theoretically, a malicious user could spam a login page with likely user accounts to force account lockouts for a large number of website users. After the user accounts are locked out, the users have no way to get back onto the website until an administrator intervenes and unlocks the accounts.

454

SqlMembershipProvider

Although an auto-unlock feature for accounts is a partial deterrent to this type of DoS attack, you should be aware that after you have automatic unlocking, the DoS attack can now be turned into a long-running brute force password attack. Instead of cutting the attack off after a few attempts per-user account, an auto-unlock feature allows an attacker to iterate through a few passwords, back off for the duration of the account lockout, and then iterate through some more passwords for each user account. If you don’t monitor web logs (and potentially add custom auditing on top of the SQL provider) for this type of activity, you can literally end up with a brute force password attack running for weeks on end.

For example, if you have a 30-minute auto-unlock period after five bad passwords, and an attacker tries guessing passwords for 4 weeks, the attacker can run 240 bad passwords per account per day for a rough total of 6720 bad passwords per user account per month on a site. I would highly recommend that if you add automatic unlock behavior as shown in this section that you also implement additional security measures to mitigate a long-running password guessing attack. Even if an attacker never successfully guesses a password because of password strength rules, a long-running password-guessing attack can also look like a denial of service attack because each user account that is being attacked ends up in a locked-out state for the vast majority of the time. Other than for a few seconds at the expiration of the auto-unlock period, accounts end up locked out again when the password-guessing attack sweeps through the same set of accounts on its next iteration. And, of course, a really savvy attacker will probably only guess (lockout limit –1) passwords at a time for a user, thus keeping a long-running password guessing attack below the radar if you are only looking at rates of account lockouts.

As a result, the best argument for implementing auto-unlocking is as a convenience for sites that are already partially protected against brute force attacks by other security measures. For example, if you run your site under SSL, then a brute force attack is less likely due to the increased likelihood that the spike in SSL processing overhead from an attack would be detected by the site’s administrators. If your website is only accessible over VPNs or private frame relay networks, the likelihood of a random attacker getting in and wreaking havoc is lower In these cases, automatic unlock behavior provides a better user experience and cuts down on password-related support calls.

A custom provider that implements auto-unlock behavior needs a place for users to configure the timeout beyond which the provider should automatically unlock the user account. For this example, you want the provider configuration to look like the following:

<add name=”autounlocksample” type=”AutoUnlockProvider” connectionStringName=”LocalSqlServer” autoUnlockTimeout=”30” applicationName=”passwordHistory”/>

The custom attribute autoUnlockTimeout tells the provider how many minutes after a lockout a user account should be automatically unlocked. The provider stores this attribute inside of an override of the

Initialize method:

using System;

using System.Configuration.Provider; using System.Web.Security;

public class AutoUnlockProvider : SqlMembershipProvider

{

private int autoUnlockTimeout = 60; //Default to 60 minutes

public override void Initialize(string name,

455

Chapter 11

System.Collections.Specialized.NameValueCollection config)

{

string sunlockTimeOut = config[“autoUnlockTimeout”]; if (!String.IsNullOrEmpty(sunlockTimeOut))

autoUnlockTimeout = Int32.Parse(sunlockTimeOut); config.Remove(“autoUnlockTimeout”);

base.Initialize(name, config);

}

//other overrides

}

Before calling the base class Initialize method, the custom provider looks for the autoUnlockTimeout attribute in configuration. If it finds the attribute, it stores its value and removes it from the configuration collection. If the attribute is not supplied in the provider’s configuration, it defaults to a 60-minute long timeout after which locked accounts can be automatically unlocked.

Because there are a number of different provider methods that should automatically unlock the user, the core functionality is implemented in a single private method:

private bool AutoUnlockUser(string username)

{

MembershipUser mu = this.GetUser(username,false); if ((mu != null) &&

(mu.IsLockedOut) && (mu.LastLockoutDate.ToUniversalTime().AddMinutes(autoUnlockTimeout)

< DateTime.UtcNow)

)

{

bool retval = mu.UnlockUser(); if (retval)

return true;

else

return false; //something went wrong with the unlock

}

else

return false; //not locked out in the first place //or still in lockout period

}

For any given username, this method loads the MembershipUser instance for that user. If the MembershipUser instance indicates that the user is locked out, the provider checks to see how much time has elapsed since that last lockout. If more than autoUnlockTimeout minutes have elapsed, the method calls UnlockUser to automatically unlock the account. The return value from the method indicates whether the user account was unlocked. Normally, calling this method for users still within the autoUnlockTimeout period returns false, whereas calling the method for users who are past the timeout period results in a true return value.

To demonstrate how this method works with methods that deal with passwords, the following code shows ValidateUser automatically unlocking users as necessary:

456

SqlMembershipProvider

public override bool ValidateUser(string username, string password)

{

bool retval = base.ValidateUser(username, password);

//The account may be locked out at this point if (retval == false)

{

bool successfulUnlock = AutoUnlockUser(username); if (successfulUnlock)

//re-attempt the login

return base.ValidateUser(username, password);

else

return false;

}

else

return retval; //first login was successful

}

First, the custom provider lets the base provider attempt to validate the user’s credentials. If the base call succeeds, no further work is necessary. However, if the initial result is false, the method attempts to unlock the user. There may be other reasons why ValidateUser fails — for example, the user account specified by username may not even exist in the Membership database. If the unlock attempt succeeds though, then custom provider again calls the base class’s ValidateUser. This sequence of calls will usually result in the second attempt succeeding, assuming, of course, that that password parameter is valid. If the automatic unlock attempt did not succeed, then the custom provider returns false because there isn’t any point in calling base.ValidateUser again for a user that is still locked out.

The same implementation pattern can be used with the password-related methods ChangePassword and ChangePasswordQuestionAndAnswer. The overrides for these methods looks the same as the ValidateUser override with the one difference being that the calls to the base class use the appropriate method. With the custom ValidateUser implementation, you can try logging in with an account and intentionally force a lockout. After autoUnlockTimeout minutes pass, the next call to ValidateUser will succeed if you supply the correct password. In fact, this functionality also works transparently with a control like the Login control. This is another example of how provider customization can be completely transparent to the user interface layer.

The other aspect of automatically unlocking users is in methods that deal with password answers. The override for ResetPassword is:

public override string ResetPassword(string username, string passwordAnswer)

{

//A MembershipPasswordException could be due to a lockout try

{

return base.ResetPassword(username, passwordAnswer);

}

catch (MembershipPasswordException me) {}

bool successfulUnlock = AutoUnlockUser(username); if (successfulUnlock)

457

Chapter 11

//re-attempt the password reset

return base.ResetPassword(username, passwordAnswer);

else

throw new ProviderException(

“The attempt to auto unlock the user failed during ResetPassword.”);

}

In this case, the ResetPassword method will throw a MemershipPasswordException if the user is locked out. As a result, the first call to the base class is wrapped in a try-catch block that suppresses this exception. In the event that the user is locked out, the override calls AutoUnlockUser to attempt to unlock the user account. If the user account was successfully unlocked, the custom provider attempts to reset the password again by calling into the base class. However, if the automatic unlock attempt failed for some reason, it throws a ProviderExpcetion to alert callers to the fact that the reset attempt failed. You could also choose to rethrow the MembershipPasswordException if you put extra logic into AutoUnlockUser to determine exactly why the unlock attempt failed.

If you use a sample page that calls ResetPassword, you can intentionally supply five bad password answers to cause the user account to be locked out. As with ValidateUser, if you now wait autoUnlockTimeout minutes to pass, the next call to ResetPassword with a valid answer will succeed. Note though, unlike the Login control, if you use the PasswordRecovery control with this custom provider the PasswordRecovery control is unable to load the MembershipUser object for a locked-out user. Therefore, you will need to customize the PasswordRecovery control to work with the automatic unlock logic in the custom provider. The GetPassword method in the custom provider implements the same logic shown for ResetPassword. The only difference, of course, is that the GetPassword method calls base.GetPassword in the appropriate places. Overall though, you can see how straightforward it is to add automatic unlock logic to SqlMembershipProvider with a little bit of code. The best part is that you can implement this functionality using publicly available APIs, so you don’t have to worry about any future changes in the provider breaking your custom code.

Suppor ting Dynamic Applications

Normally, an instance of SqlMembershipProvider knows which application name to use by looking at the value of the applicationName configuration attribute. The default configuration in machine.config sets applicationName to /, so most developers will probably want to explicitly redefine membership providers in their applications to use a more suitable name. Many of the previous examples of extending SqlMembershipProvider showed configurations that used more appropriate values for applicationName.

The one constraint on the applicationName attribute though is that it is statically defined. After you set the value in configuration, the provider remembers that value for the rest of its lifetime. If you look at the MembershipProvider base class definition, though, you see that the ApplicationName property for the provider is abstract and that a setter is also defined. Concrete providers like SqlMembershipProvider can choose to implement the setter so that developers can change the application name at runtime.

This means that you can write code that switches between different application data living in the same Membership table with code like the following:

458

SqlMembershipProvider

(SqlMembershipProvider)p = Membership.Provider; //assume default provider is SQL p.ValidateUser(“someuser”,”somepassword”);

p.ApplicationName = “A_Different_Value_Than_Configuration”;

p.ValidateUser(“some other user”,”password”);

Supporting the setter for ApplicationName can actually be quite useful for single-threaded applications. For example, if you used an application like the console application shown in the previous chapter for creating users, you could easily pass the desired application name as a command-line argument and then set this value on the provider instance. In this way, the create user console application would have no hard-coded dependencies on the application name.

The flaw with this approach is that in any kind of multithreaded environment, such as ASP.NET, it is likely that multiple pages will be running simultaneously. If two pages both have code like that in the preceding example, which one wins? Remember that each configured provider is instantiated only once and that the same instance is used by all threads in an ASP.NET application. The answer to this question for SqlMembershipProvider is that it depends:

At best, no corruption of the internal application name variable occurs, and the two pages run in just the correct sequence that each page works with the correct application name value.

One page stomps on the application name value that was just set by the other page, and as a result one of the two pages ends up working with the wrong set of data.

The worst-case scenario is that both pages attempt to update the provider’s private application name variable, with unknown results. This outcome would probably occur intermittently on a multiprocessor machine where you not only have threads logically running in parallel, but you also physically have different threads running simultaneously on different processors. The “nice” thing about this outcome is that it would probably only occur intermittently under stress, so you would go nuts trying to reproduce the problem!

The ASP.NET development team had considered at one point adding some locking to the get and set properties in SqlMembershipProvider’s ApplicationName property. However, the setter for this property was not really intended to support dynamically switching application names in a high-concur- rency application like ASP.NET. Even if the locking semantics were added, you would end up with a “hot” lock. Developers who wrote web applications that constantly set and reset the application name would find that a fair amount of time was being spent entering and exiting a lock section around the application name variable.

Even if the team had added locking, it still wouldn’t prevent multiple pages running simultaneously from overwriting each other’s application name. It is the old problem with the Singleton pattern — access to shared state not only has to be serialized, but any operations that depend on the shared state are also liable to cause errors if the intent was that the change to shared state was supposed to be private to the calling thread.

The solution to this problem in ASP.NET 2.0 was to make the ApplicationName property abstract. Although SqlMembershipProvider doesn’t take advantage of this fact, you can. If you have an application where each page request needs to run in the context of a specific application name, and you want SqlMembershipProvider to dynamically use the correct application name, then you need to write a custom provider that overrides the ApplicationName getter. You can leave the setter alone because

459

Chapter 11

internally SqlMembershipProvider never uses it. Common scenarios that require this type of dynamic functionality are portal applications where one ASP.NET app-domain may actually be serving up multiple virtual “applications.” In this type of scenario, it would be incredibly unwieldy to have to register a separate provider instance for each application — and in the case of self-registered “applications,” you wouldn’t even be able to use a configuration-driven approach.

You have two design choices for the ApplicationName override. You can make the provider directly aware of contextual information for the request that determines the correct value for application name. Or you can write some other code (for example, an HttpModule) that processes information from a request and then stores the resulting application name in a convenient location such as HttpContext. For this sample, I use the latter approach. From an architectural perspective, you probably don’t want a custom provider to know all of the details about how an application name is determined. Instead, you want the provider to look at a central location that holds the code that determines the correct value neatly factored out into a separate class.

An HttpModule is the logical place to centralize the logic for determining the correct application name:

using System; using System.Web;

public class PortalApplicationProcessor : IHttpModule

{

public void Dispose()

{return; }

private void DetermineApplicationName(Object sender, EventArgs e)

{

HttpApplication app = (HttpApplication)sender; HttpContext context = app.Context;

string qAppName = app.Request.QueryString[“appname”]; if (!String.IsNullOrEmpty(qAppName))

context.Items[“ApplicationName”] = qAppName;

else

context.Items[“ApplicationName”] = “NOTSET”;

}

public void Init(HttpApplication app)

{

app.BeginRequest +=

new EventHandler(this.DetermineApplicationName);

}

}

This module hooks the BeginRequest event to ensure that the application name has been determined before anything significant, such as authentication, has occurred. The module looks on the query-string for a variable called appname. If it finds this query-string variable, it stores it in the HttpContext’s Items collection. If the query-string variable is not found, then a default value is stored in the context instead. The only link required between HttpModule and a custom provider is a common agreement on what to call the variable in HttpContext. In this example, the context variable is called ApplicationName. Although this sample uses a query-string variable, you could certainly determine the application name from a form variable, a custom HTTP header, and so on.

460

SqlMembershipProvider

The next step is to write a custom provider that overrides the ApplicationName property getter:

public class ApplicationProvider : SqlMembershipProvider

{

public override string ApplicationName

{

get

{

string appNameFromContext = (string)HttpContext.Current.Items[“ApplicationName”];

if (appNameFromContext != “NOTSET”) return appNameFromContext;

else

return base.ApplicationName;

}

}

}

The code for the custom provider is trivial. The ApplicationName property first looks in the context to see if a nondefault value for the ApplicationName variable was set. If such a value is found, the provider returns it. Otherwise, the provider reverts to the application name value stored in the provider’s configuration.

At this point, all coding necessary to support dynamic application names is complete. You can test the custom provider by configuring a test application to use the provider as well as the associated

HttpModule.

<httpModules>

<add name =”PortalProcessor” type=”PortalApplicationProcessor”/> </httpModules>

<membership defaultProvider=”portalAware”> <providers>

<add name=”portalAware” type=”ApplicationProvider” connectionStringName=”LocalSqlServer” />

</providers>

</membership>

Now that the sample application knows about the custom HttpModule, you can start authoring pages that make use of Membership in a dynamic manner. For example, you can drop the CreateUserWizard control onto a page and then request it with different URLs:

http://localhost/Chapter11/ChangingApplicationName/CreateUser.aspx?appname=fooapp2

— or —

http://localhost/Chapter11/ChangingApplicationName/CreateUser.aspx?appname=barapp

After stepping through the wizard, new users are automatically created in the Membership database and associated with different application names based on the appname query-string variable. If you use other controls like the Login control with the query-string variable, you can log in using credentials from different application names.

461