Tuesday, April 26, 2011

Default authorization filter provider

Phil Haack recently posted about Conditional Filters in ASP.NET MVC3.  It turned out to be a very timely post because I happen to have a need that nearly matches the example in his introduction.  Nearly every action, except Login, in my current application requires that the user be authenticated.  Some actions require further authorization processing, but just about everything requires that you be logged in.

In the past I would have built a base controller, applied the default Authorize attribute to the controller, then derived everything but my AccountController (where login lives) from the base controller.  This seemed like the safest way to ensure that all my actions are protected, but anytime I have a controller that has mixed public/private actions, I’m limited.  I can’t use the base controller or I have to introduce two base controllers – one with the common code and another that derives from it, but enforces authorization.  In addition, in each of these controllers with mixed actions I have to remember to protect access rather than permit access in exceptional cases.  Clearly the latter would be more secure since, as I age, I get more forgetful.

Now, using a custom FilterProvider, I can get the behavior that I prefer: a default AuthorizeAttribute [filter] applied to each action UNLESS the controller or action has some other, specific AuthorizationAttribute-based “restriction” applied.  I say “restriction” because it could be that I make the action public rather than further restrict access.  It only takes three simple steps:

First, create a DefaultAuthorizationFilterProvider.  Using Phil’s example as a template I created the following class.  Note that it implements IFilterProvider.  In GetFilters, it inspects both the controller (from the ControllerContext) and the action (from the ActionDescriptor) to see if either have an attribute that derives from AuthorizeAttribute.  If so, it simply returns an empty set of filters.  If not, it adds a default AuthorizeAttribute to the set of filters with global scope.  This will ensure that an something that derives from AuthorizeAttribute is supplied as a filter to every action.

public class DefaultAuthorizationFilterProvider : IFilterProvider
{
    public IEnumerable<Filter> GetFilters( ControllerContext controllerContext, ActionDescriptor actionDescriptor )
    {
        var filters = new List<Filter>();
        var controllerType = controllerContext.Controller.GetType();
        if (!controllerType.GetCustomAttributes( typeof( AuthorizeAttribute ), true ).Any()
            && !actionDescriptor.GetCustomAttributes( typeof( AuthorizeAttribute ), true ).Any())
        {
            filters.Add( new Filter( new AuthorizeAttribute(), FilterScope.Global, null ) );
        }
        return filters;
    }
}

Next, just as in Phil’s post, add this FilterProvider to the collection of filter providers in the Application_Start method of Global.asax.cs.  Ours is a bit simpler because, frankly, the use case is simpler.  We don’t have complex conditions to map, just check if there is a attribute or not, we can live with the default constructor and haven’t got any set up to do.
FilterProviders.Providers.Add( new DefaultAuthorizationFilterProvider() );

Finally, we want to be able to easily permit public access.  To do that, I created a PublicAttribute class that derives from AuthorizeAttribute and overrides AuthorizeCore to simply return true for every authorization request.  Yes, you can still set it up with Roles and Users, but they will be ignored.  I can live with that.
[AttributeUsage( AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true )]
public class PublicAttribute : AuthorizeAttribute
{
    protected override bool AuthorizeCore( System.Web.HttpContextBase httpContext )
    {
        return true;
    }
}

Now, to allow public access to an action we need to apply the PublicAttribute to the method (or controller). Note that in the following only the Login methods (GET and POST) are public.  You still need to be logged in to Logout (maybe we could make this public? meh) or to change your password.  And, look, we get to inherit from our BaseController, taking advantage of any common code or dependencies that we may have!

public class AccountController : BaseController
{
    [HttpGet]
    [Public]
    public ActionResult Login()
    {
        ...
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [Public]
    public ActionResult Login( LoginModel model, string returnUrl )
    {
       ...
    }

    [HttpGet]
    public ActionResult Logout()
    {
        ...
    }

    [HttpGet]
    public ActionResult ChangePassword()
    {
        ...
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult ChangePassword( ChangePasswordModel model )
    {
        ...
    }
}