Pages

Wednesday, 2 May 2012

Securing Your MVC Intranet Applications - Security by Default

The standard way to secure your MVC Intranet applications is to use the [Authorize] attribute for controller actions you want to secure. The controller is the resource you're actually trying to protect and any security decisions should be done at the controller level rather than at the route level.


For example using the default Intranet Application template in MVC3, you will have a Home Controller that looks like the following:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;


namespace MvcAuthenticationSample.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";


            return View();
        }


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

To force users to log on when they access /Home/, you would add the [Authorize] attribute to your Index ActionResult, like this:

[Authorize]

public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";


            return View();
        }

If you are using the built in Membership provider included in the Intranet Application template and set the [Authorize] attribute on the Index() ActionResult above, your users will be redirected to the LogOn action in your Account Controller. If a user accesses /Home/About/, they will not be redirected.


Hopefully you can see the flaw in this approach for an Intranet Application as a controller action may be created where the [Authorize] attribute is not set where it should be. You might think this will be mitigated by testing, but it can be easily overlooked.


The best way to secure your MVC Intranet Application is to require have all controller actions by default  require authorisation. You can then open up access to controller actions you want users to be able to access anonymously, which leads to a much better approach to security.


One way of doing this is to create a base controller with the [Authorize] attribute set, and have all your controllers derive from this base controller. However, with this approach there is nothing to stop a controller being created that doesn't derive from the base controller you have created.


A much better way to do this is to use Global Filters. This allows you to set a global filter for all controllers in your application, which means they will have the [Authorize] attribute set by default, whether or not they derive from a base controller. Coupled with this, you can then create a custom attribute which you set explicitly for controller actions that do not require authorisation.


To do this, first you need to create your custom allow anonymous attribute:


using System;


[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AllowAnonymousAttribute : Attribute { }

You will also need to create a filter that derives from AuthorizeAttribute:

public sealed class LogonAuthorize : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true);
            if (!skipAuthorization)
            {
                base.OnAuthorization(filterContext);
            }
        }
    }

You will then need to register this new filter in RegisterGlobalFilters in global.asax:

        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new LogonAuthorize());
        }

And voila! Your controllers will require authorisation by default. However...

With this filter registered, you will not be able to log on! This is where you need to set you AllowAnonymous attribute in your Account Controller to allow users to access actions associated with logging in to your application:

        [AllowAnonymous]
        public ActionResult LogOn()
        {
            return View();
        }

        //
        // POST: /Account/LogOn

        [HttpPost]
        [AllowAnonymous]
        public ActionResult LogOn(LogOnModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                if (Membership.ValidateUser(model.UserName, model.Password))
                {
                    FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
                    if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index", "Home");
                    }
                }
                else
                {
                    ModelState.AddModelError("", "The user name or password provided is incorrect.");
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

I would personally like to see future version of MVC intranet application templates implement a secure by default approach to controllers. Inadvertently creating security holes in applications can be a big problem that is difficult to rectify.

A sample project can be downloaded from here.



No comments:

Post a Comment