Implement Expiring Passwords with Membership and Role Providers in ASP.Net 2.0

I recently had a requirment come accross my desk that required user passwords to expire after a certain period of time.  Upon expiration, the user is required to change their password and cannot perform any function until they have changed their password.  The existing system was already using the Membership and Role Providers with great success.  I wanted to implement this new requirement with minimal impact to the system.  I did a few quick Google searchs and did not find existing solutions (if I can't find something on Google within 60 seconds of searching then its not out there :)  I want to share my solution with the masses and get feedback on other implementaions currently out there.

To implement expiring passwords we need an additional data element associated with each user, the date/time at which their password will expire.  I didn't want to add a new table just to store this information.  I briefly considered using the Profile API (you may choose to modify my solution to do just that) but we weren't currently using it and I saw no reason to introduce that.  Looking at the Membership Provider I saw a lovely little field called "Comment" that was currenly unused...this would be perfect for our uses.

So, we have half the problem solved.  Anytime a user changes their password, we will set their password expiration date 30 days into the future and store this value in the Comment field of the Membership Provider.  Now, when a user has an expired password, how do we prevent them from doing anything until they change their password?

You have all been good developers and added a layer of abstraction to your pages by implementing a custom base page that all of your pages inherit from right?  If you haven't, shame on you and go do that before you finish the article.  If you have, then our implementation is pretty straight forward.  Simply override your OnInit method in your base page, check if the users password is expired, if so, redirect them to the change password page.  Since you are modifying your base page, if the user tries to navigate to any page other than change password, they will be immidiately redirected to the Change Password page.  Viola.

I actually took my implementation a tiny step further.  Whenever a user's password expires, I add them to a Role called "Must Change Password".  Our system provides an interface that allows administrator's to manage a users' roles.  So now an administrator can force a password change simply by adding the user(s) to this role.  Because roles can be cached via cookie on the user's machine, you do not need to access the database on every page to determine if a user's password has expired, simple check for the precense of the proper role.

You can download the goods here.

ExpiringMembershipUser.cs

using System;
using System.Configuration;
using System.Globalization;
using System.Web.Security;
 
namespace ExpiringPasswords
{
  public class ExpiringMembershipUser
  {
    private readonly MembershipUser _membershipUser;
    public static readonly string ExpiredPasswordRole;
    public static readonly int PasswordExpiresAfterDays;
 
    /// <summary>
    /// Initializes the <see cref="ExpiringMembershipUser"/> class.  Reads and 
    /// validates configuration values from the web.config file.
    /// </summary>
    static ExpiringMembershipUser()
    {
      // Expired Password Role
      ExpiredPasswordRole = ConfigurationManager.AppSettings["ExpiredPasswordRole"] ?? string.Empty;
      if (!Roles.RoleExists(ExpiredPasswordRole))
      {
        throw new ConfigurationErrorsException(
          "Please add '<add key=\"ExpiredPasswordRole\" value=\"{Your Role Here}\" />' " +
          "to the appSettings section of your web.config file and ensure you have created " +
          "the role with the ASP.Net Role Manager");
      }
 
      // Password Expires After Days
      string sPasswordExpiresAfterDays = ConfigurationManager.AppSettings["PasswordExpiresAfterDays"];
      if (string.IsNullOrEmpty(sPasswordExpiresAfterDays))
      {
        PasswordExpiresAfterDays = 30;
      }
      else
      {
        bool validValue = Int32.TryParse(sPasswordExpiresAfterDays, out PasswordExpiresAfterDays);
        if (!validValue || PasswordExpiresAfterDays <= 0)
        {
          throw new ConfigurationErrorsException(string.Format(
            "Invalid value '{0}' for appSetting 'PasswordExpiresAfterDays'",
            sPasswordExpiresAfterDays));
        }
      }
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ExpiringMembershipUser"/> class.
    /// </summary>
    /// <param name="membershipUser">The membership user.</param>
    public ExpiringMembershipUser(MembershipUser membershipUser)
    {
      if (membershipUser == null) throw new ArgumentNullException("membershipUser");
 
      _membershipUser = membershipUser;
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ExpiringMembershipUser"/> class.
    /// </summary>
    /// <param name="username">The username.</param>
    public ExpiringMembershipUser(string username)
    {
      _membershipUser = EnsureMembershipUser(username);
    }
 
    /// <summary>
    /// Gets or sets the password expiration date in UTC time.  
    /// When setting this value, it will be converted to UTC time.
    /// </summary>
    /// <value>The password expiration date.</value>
    public virtual DateTime PasswordExpiresOnUTC
    {
      get
      {
        string sPasswordExpiresOn = _membershipUser.Comment;
        DateTime dtPasswordExpiresOn = DateTime.Now.AddDays(PasswordExpiresAfterDays).ToUniversalTime();
 
        if (!DateTime.TryParse(sPasswordExpiresOn, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out dtPasswordExpiresOn))
        {
          _membershipUser.Comment = dtPasswordExpiresOn.ToString();
          Membership.UpdateUser(_membershipUser);
        }
 
        return dtPasswordExpiresOn;
      }
      set
      {
        _membershipUser.Comment = value.ToUniversalTime().ToString();
      }
    }
 
    /// <summary>
    /// Gets a value indicating whether [password expired].
    /// </summary>
    /// <value><c>true</c> if [password expired]; otherwise, <c>false</c>.</value>
    public virtual bool PasswordExpired
    {
      get
      {
        return PasswordExpiresOnUTC < DateTime.Now.ToUniversalTime();
      }
    }
 
    /// <summary>
    /// Expires the password.
    /// </summary>
    public virtual void ExpirePassword()
    {
      PasswordExpiresOnUTC = DateTime.Now;
      if (!Roles.IsUserInRole(_membershipUser.UserName, ExpiredPasswordRole))
      {
        Roles.AddUserToRole(_membershipUser.UserName, ExpiredPasswordRole);
      }
      Membership.UpdateUser(_membershipUser);
    }
 
    /// <summary>
    /// Unexpires the password.
    /// </summary>
    public virtual void UnexpirePassword()
    {
      PasswordExpiresOnUTC = DateTime.Now.AddDays(PasswordExpiresAfterDays);
      if (Roles.IsUserInRole(_membershipUser.UserName, ExpiredPasswordRole))
      {
        Roles.RemoveUserFromRole(_membershipUser.UserName, ExpiredPasswordRole);
      }
      Membership.UpdateUser(_membershipUser);
    }
 
    /// <summary>
    /// Expires the password if expired.
    /// </summary>
    /// <param name="username">The username.</param>
    public static void ExpirePasswordIfExpired(string username)
    {
      if (!IsPasswordExpired(username))
      {
        ExpiringMembershipUser expiringMembershipUser = new ExpiringMembershipUser(username);
        if (expiringMembershipUser.PasswordExpired)
        {
          expiringMembershipUser.ExpirePassword();
        }
      }
    }
 
    /// <summary>
    /// Determines whether the specified user's password is expired.
    /// </summary>
    /// <param name="username">The username.</param>
    /// <returns>
    ///   <c>true</c> if [is password expired] [the specified username]; otherwise, <c>false</c>.
    /// </returns>
    public static bool IsPasswordExpired(string username)
    {
      return Roles.IsUserInRole(username, ExpiredPasswordRole);
    }
 
    /// <summary>
    /// Unexpires the password.
    /// </summary>
    /// <param name="username">The username.</param>
    public static void UnexpirePassword(string username)
    {
      ExpiringMembershipUser expiringMembershipUser = new ExpiringMembershipUser(username);
      expiringMembershipUser.UnexpirePassword();
    }
 
    /// <summary>
    /// Ensures the membership user.
    /// </summary>
    /// <param name="username">The username.</param>
    /// <returns></returns>
    protected static MembershipUser EnsureMembershipUser(string username)
    {
      if (username == null) throw new ArgumentNullException("username");
 
      MembershipUser membershipUser = Membership.GetUser(username);
      if (membershipUser == null)
      {
        throw new InvalidOperationException(string.Format("No user exists for username '{0}'", username));
      }
 
      return membershipUser;
    }
  }
}

Example BasePage.cs

using System;
using System.Web;
using System.Web.UI;
 
namespace ExpiringPasswords
{
  public class BasePage : Page
  {
    protected virtual bool CheckForExpiredPassword
    {
      get { return true; }
    }
 
    protected override void OnInit(EventArgs e)
    {
      base.OnInit(e);
 
      if (CheckForExpiredPassword)
      {
        if (ExpiringMembershipUser.IsPasswordExpired(HttpContext.Current.User.Identity.Name))
        {
          Response.Redirect("ChangePassword.aspx?Message=Your password has expired and must be changed");
          Response.End();
        }
      }
    }
  }
}

Example Login.aspx.cs

using System;
 
namespace ExpiringPasswords
{
  public partial class Login : BasePage
  {
    protected override bool CheckForExpiredPassword
    {
      get { return false; }
    }
 
    protected override void OnInit(EventArgs e)
    {
      base.OnInit(e);
 
      if (!IsPostBack)
      {
        login.Focus();
      }
    }
 
    protected virtual void login_LoggedIn(object s, EventArgs e)
    {
      ExpiringMembershipUser.ExpirePasswordIfExpired(login.UserName);
    }
  }
}

Example ChangePassword.aspx.cs

using System;
 
namespace ExpiringPasswords
{
  public partial class ChangePassword : BasePage
  {
    protected override bool CheckForExpiredPassword
    {
      get { return false; }
    }
 
    protected override void OnInit(EventArgs e)
    {
      base.OnInit(e);
 
      string message = Request.QueryString["Message"];
      if (!string.IsNullOrEmpty(message))
      {
        changePassword.ChangePasswordTitleText = message;
      }
 
      if (!IsPostBack)
      {
        changePassword.Focus();
      }
    }
 
    protected virtual void changePassword_OnChangedPassword(object s, EventArgs e)
    {
      ExpiringMembershipUser.UnexpirePassword(changePassword.UserName);
    }
  }
}

posted @ Friday, January 05, 2007 10:00 AM


Print

Comments on this entry:

# re: Implement Expiring Passwords with Membership and Role Providers in ASP.Net 2.0

Left by Willie at 1/5/2007 7:48 PM

Like always I'm thinking lazy. Couldn't you at login check to see if the password has expired? If so, then take to a screen to change it up? This would essentially combine the login with another call to the database to check the date. Right?

# re: Implement Expiring Passwords with Membership and Role Providers in ASP.Net 2.0

Left by Bill Pierce at 1/6/2007 6:43 AM

You could definitely do that however what would prevent the user from navigating away from the change password screen?

# re: Implement Expiring Passwords with Membership and Role Providers in ASP.Net 2.0

Left by Willie at 1/8/2007 10:05 AM

Essentially you have the one page that isn't secure (Default.aspx). Upon login, before authentication, you check the date the password had changed before. If so, bring up a panel or whatnot on the same page, and don't authenticate the user. This way, they won't be able to login until the change is made. You could do something similar when the password is about to change ... bring up a alert that says, "Your password will change in x amount of days. Wanna change it?"

# re: Implement Expiring Passwords with Membership and Role Providers in ASP.Net 2.0

Left by Bijay at 8/16/2007 9:57 PM

We are looking for similar solution.But we are using ActiveDirecory. Looks like membershp doesn't allow change expired password on Active Directory.

Any suggestion or pointers on doing this will be appreciated.

Your comment:



 (will not be displayed)


 
 
 
Please add 3 and 5 and type the answer here:
 

Live Comment Preview:

 
«August»
SunMonTueWedThuFriSat
272829303112
3456789
10111213141516
17181920212223
24252627282930
31123456