Implementing Automatic Unlocking in ASP.NET 2.0 SqlMembershipProvider

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.

A custom Membership 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,
      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 provider 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:

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.

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. For example 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)
      //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.

To test this method, you can use a sample page that calls ResetPassword and 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.

This article is adapted from Professional ASP.NET 2.0 Security, Membership, and Role Management by Stefan Schackow (Wrox, 2006, ISBN: 0-7645-9698-5), from Chapter 11 " SqlMembershipProvider." Reprinted with permission from the publisher.



About the Author

Stefan Schackow

Stefan Schackow currently works as a program manager at Microsoft on the ASP.NET product team. He has worked extensively with the new application services delivered in ASP.NET 2.0, including Membership and Role Manager. Prior to joining the ASP.NET product team, he worked in Microsoft's consulting services designing web and database applications for various enterprise clients.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Hurricane Sandy was one of the most destructive natural disasters that the United States has ever experienced. Read this success story to learn how Datto protected its partners and their customers with proactive business continuity planning, heroic employee efforts, and the right mix of technology and support. With storm surges over 12 feet, winds that exceeded 90 mph, and a diameter spanning more than 900 miles, Sandy resulted in power outages to approximately 7.5 million people, and caused an estimated $50 …

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds