Writing a custom Authorize attribute for MVC 2/3

Microsoft's MVC (1/2/3/4/5) has these really cool attributes to decorate controllers and actions with. The nice thing about these attributes is that they affect how a given controller or action behaves. One of the built-in attributes is Authorize, which checks if the user is logged in. If this is not the case, the controller or action will not be executed and the user will be redirected to the login page (or whatever is defined in the web.config). An example of this attribute is:

[Authorize]
public class HomeController : BaseController
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Error()
    {
        return View();
    }
}

In this case, none of the controller's actions will be fired when the user is not logged in. This attribute does, however, assume that you are using Microsoft's Membership Provider. If you use the default authentication method in WebForms or MVC, you'll be fine. But what if you have your own security system? You could of course create a custom membership provider. But that's a lot of work, and sometimes the membership provider just doesn't cut it. For many of NowOnline's projects, we use a custom OpenID Provider. This system is based on an open source .NET implementation of OpenID, which allows single sign-on and federated logins. Very useful (and a good topic for another post), but how would you implement a custom Authorize attribute?

Just inherit

Thankfully, creating a custom attribute to do this sort of thing is very, very, easy. The only thing you have to do is create a custom class and inherit from the AuthorizeAttributeclass that lives in System.Web.Mvc. You'll then have to override the OnAuthorizationmethod to implement your own logic. This method should return a HttpUnauthorizedResultwhen the user is not allowed. In that case, the user will be automatically redirected to the login page. This is what a custom Authorize attribute might look like:

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null) { throw new ArgumentNullException("filterContext"); }

        // check if user has access and return HttpUnauthorizedResult when user is not allowed
        if (/* custom logic */)
        {
            filterContext.Result = new HttpUnauthorizedResult();  
        }

        SetCachePolicy(filterContext);
    }

    public void CacheValidationHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
    }

    protected void SetCachePolicy(AuthorizationContext filterContext)
    {      
        // disable caching, so that a user that is not logged in cannot accidentally get a cached version of a secure page
        HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
        cachePolicy.SetProxyMaxAge(new TimeSpan(0));
        cachePolicy.AddValidationCallback(CacheValidationHandler, null /* data */);
    }}

The top method overrides OnAuthorizeand performs it's own check to see if the user is allowed to proceed. You have to implement the check yourself, based on the system that you use. For example, for our OpenID framework we check if the user is logged in for the Membership Provider. But in addition, we also check if the OpenID appears on a whitelist for the current application. Using the method is easy as cake, just do this (or something like this):

[CustomAuthorize]
public class HomeController : BaseController
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Error()
    {
        return View();
    }
}

The devil is in the caching

But don't implement your custom attribute without the bottom two methods in the example! A nasty thing that might happen with webservers, especially when there are proxyservers involved, is that either the webserver or the proxy caches pages that are supposed to be sensitive. What's wrong with that, you might ask? Well, this means that a user that is not logged on could accidentally be allowed to view a cached paged that is supposed to be secure. Obviously, this is not good thing. So i've these methods to block caching for sensitive pages. That's why SetCachePolicy is only called after the validation has taken place. If the user failed the login check, the resulting page can be cached.

Conclusion

So there you have it; your own custom attribute. Very easy to implement (except maybe for the caching thing) and very easy to extend. You can do this for all attributes to are shipped with MVC. But i'll give you more examples in later posts.

Christiaan Verwijs
Christiaan Verwijs

Scrum Master, Trainer, Developer & founder of Agilistic