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);
    }
  }
}
«January»
SunMonTueWedThuFriSat
31123456
78910111213
14151617181920
21222324252627
28293031123
45678910