A recent set of requirements I’ve been playing with deals with passwords. This one specifically handles password expiration.
Given that I’m working with ASP.NET MVC I know I can rest assured that there’s some great (read awesome) way of implementing a given requirement. This is exactly what happened and I want to show you how to have a clean and beautiful solution to this problem.
So my client’s requirement is the following:
Passwords should expire in 45 days.
I’m currently using the default ASP.NET membership provider. It gives you a database schema ready to manage users and roles. You just have to use ASP.NET Configuration Tool to create Roles and Users, decorate your Controllers/Actions with the Authorize attribute and you’re good to go most of the time. The default membership provider allows a fast project start – no doubt – but as always there’s something that must be done according to the infinitude of possible requirements that change project by project. One of these not contemplated things is a setting in the default provider for handling user’s password expiration. We have to roll our own code to manage this.
My friend Google told me that some folks have already done some work related to this and as always I borrow some of their code and adapt it to my specific case/technology.
First I created a PasswordExpiredAttribute that derives from/extends the AuthorizeAttribute. Here’s its code:
public class PasswordExpiredAttribute : AuthorizeAttribute
{
private static readonly int PasswordExpiresInDays =
int.Parse(ConfigurationManager.AppSettings["PasswordExpiresInDays"]);
public override void OnAuthorization(AuthorizationContext filterContext)
{
IPrincipal user = filterContext.HttpContext.User;
if(user != null && user.Identity.IsAuthenticated)
{
MembershipUser membershipUser = Membership.GetUser();
TimeSpan ts = DateTime.Today - membershipUser.LastPasswordChangedDate;
// If true, that means the user's password expired
// Let's force him to change his password before using the application
if (ts.TotalDays > PasswordExpiresInDays)
{
filterContext.HttpContext.Response.Redirect(
string.Format("~/{0}/{1}?{2}", MVC.Account.Name, MVC.Account.ActionNames.ChangePassword,
"reason=expired"));
}
}
base.OnAuthorization(filterContext);
}
}
As you see, the code goes inside the OnAuthorization overridable method. I get the PasswordExpiresInDays setting from the Web.config <appSettings> section. This gives an easy way to change the requirement in the future without the need of recompiling the whole app.
<appSettings>
<add key="PasswordExpiresInDays" value="45" />
</appSettings>
The code explains itself but let’s go through it:
1 - If the User is authenticated, let’s get his membership data;
2 - A TimeSpan is useful for getting the difference in days between Today and the last time the user changed his password ( LastPasswordChangedDate )
3 - Check if the TimeSpan.TotalDays is greater than the PasswordExpiresInDays setting we got from the Web.config file. If true the user must change his password and we redirect him to the ChangePassword view.
Note 1: I’m using T4MVC to retrieve the Controller and Action names in the code above. You should take a look at it! Really…
Note 2: See that "reason=expired" in the response redirect URL? I’m using this querystring as a route parameter inside the ChangePassword action method to display a message to the user informing him that he’s being asked to change the password because it has expired.
/// <summary>
/// This allows the logged on user to change his password.
/// </summary>
public virtual ActionResult ChangePassword(string reason)
{
var viewModel = new ChangePasswordViewModel();
if (reason != null)
{
ShowMessage(Infrastructure.Notification.MessageType.Warning, Localization.PasswordExpired, true);
}
return View(viewModel);
}
By the way: I use MvcNotification infrastructure by Martijn Boland to display beautiful messages to the user.
OK, getting back to the main point… now it’s just a matter of applying the PasswordExpiredAttribute filter to every controller of the app but the AccountController. With ASP.NET MVC 3 it’s easy to apply a filter to every controller and action using GlobalFilters. Instead of going controller by controller to add this attribute we can just register it as a global filter in the Global.asax file:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new AuthorizeAttribute());
filters.Add(new PasswordExpiredAttribute());
}
Doing so the PasswordExpiredAttribute will be executed for every controller and action but there’s a problem with the above approach. Since it’s a global filter, it’ll be executed even for the AccountController. Remember: we don’t want it to be executed for the AccontController… How can we exclude a global filter from a single controller or action? To achieve this, there’s an awesome thing we can do: create a ExcludeFilterAttribute and a ExcludeFilterProvider. WOW, ASP.NET MVC has a Filter Provider that gives us even more power when working with filters. Look here for the complete story: Exclude a Filter by Ori Calvo. I’ve uploaded the source code files here: ExcludeFilterAttribute.cs and ExcludeFilterProvider.cs
Now it’s just a matter of decorating the AccountController with the ExcludeFilter attribute like this:
[ExcludeFilter(typeof(PasswordExpiredAttribute))]
public partial class AccountController : BaseController
{
...
}
The ExcludeFilter attribute explicitly tells the ASP.NET MVC runtime to ignore the PasswordExpiredAttribute for the AccountController.
With this in place, once the logged in user tries to access any part of the site and his password is expired, he'll be redirected to the ChangePassword view and won't be allowed access to anywhere else in the site until he changes the password. This is great and the requirement is implemented.
Of course in software there are multiple ways of doing the same thing. If you know of any better option, please share you knowledge in the comments.
Hope it helps.
Bonus
While working on this requirement I posted a question at StackOverflow regarding the use of Web.config settings as magic strings. I’ve found a nice way to let the code a little bit cleaner. So, if you want a nice way to access your Web.config app settings as properties with compile time checking and nice error handling, you can do as described here: T4MVC for Web.config <appSettings>
This is a much better/cleaner code IMO (see the AppSettings class that was automatically generated with the T4 template):
public class PasswordExpiredAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
IPrincipal user = filterContext.HttpContext.User;
if (user != null && user.Identity.IsAuthenticated)
{
MembershipUser membershipUser = Membership.GetUser();
TimeSpan ts = DateTime.Today - membershipUser.LastPasswordChangedDate;
// If true, that means the user's password expired
// Let's force him to change his password before using the system
if (ts.TotalDays > int.Parse(AppSettings.PasswordExpiresInDays))
{
filterContext.HttpContext.Response.Redirect(
string.Format("~/{0}/{1}?{2}", MVC.SGAccount.Name, MVC.SGAccount.ActionNames.ChangePassword,
"reason=expired"));
}
}
base.OnAuthorization(filterContext);
}
}
References:
ASP.NET MVC Authentication - Global Authentication and Allow Anonymous by Jon Galloway
ASP.NET MVC Authentication - Customizing Authentication and Authorization The Right Way by Jon Galloway
Exclude a Filter by Ori Calvo
Introducing System.Web.Providers - ASP.NET Universal Providers for Session, Membership, Roles and User Profile on SQL Compact and SQL Azure by Scott Hanselman
Conditional Filters in ASP.NET MVC 3 by Phil Haack
T4MVC by David Ebbo