Unit Testing ASP.NET MVC Authorization

To restrict access to an ASP.NET MVC view we restrict access to the controller action that renders the view. We do this by decorating the controller and/or controller action with [Authorize] and [AllowAnonymous] attributes.

Note. We can also apply the Authorize filter globally by adding it to applications GlobalFiltersCollection.

Below is an example of a controller where I have restricted access as follows –

  1. Decorated the controller with the [Authorize] attribute.
  2. Decorated the Index() action with the [AllowAnonymous] attribute, which overrides the controller’s [Authorize] attribute, allowing all users to access the Index view.
  3. Not decorated the Details(int id) action. It therefore inherits the controller’s [Authorize] attribute, allowing only authenticated users to access the Details view.
  4. Decorated both the Create() and Create(FormCollection collection) actions with the [Authorize(Roles = “Admin”, Users = “Ross”)] attribute. This further restricts access to only authenticated users who are either associated with the Admin role or whose user name is Ross.
using System.Web.Mvc;

namespace BikeStore.Controllers
{
    [Authorize]
    public class WidgetController : Controller
    {
        // GET: Widget
        [AllowAnonymous]
        public ActionResult Index()
        {
            return View();
        }

        // GET: Widget/Details/5
        public ActionResult Details(int id)
        {
            return View();
        }

        // GET: Widget/Create
        [Authorize(Roles = "Admin", Users = "Ross")]
        public ActionResult Create()
        {
            return View();
        }

        // POST: Widget/Create
        [HttpPost]
        [Authorize(Roles = "Admin", Users = "Ross")]
        public ActionResult Create(FormCollection collection)
        {
            try
            {
                // TODO: Add insert logic here

                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }
    }
}

Security is a high priority for virtually all applications. When I deploy code for user acceptance testing I want to do so having fully unit tested the authorization.

When a request enters the MVC pipeline, the authorization filters are applied prior to a controller action being executed. The authorization filters are part of the MVC framework, and as such we do not need to test them. We can assume that they work as described. Instead what we need to do is test that the controller actions have had the authorization attributes correctly applied.

The AuthorizationTest class – shown below – contains a number of helper methods that utilize reflection to validate controller action’s authorization.

The IsAnonymous method validates whether a controller action method allows anonymous access. The method receives an instance of the controller, a method name, and an array of parameter types. The array of parameter types is used to differentiate between overloaded methods – a common practice in MVC. If a method is not overloaded then this parameter can be set to null.

For a controller action to allow anonymous access, either,

  1. the action must be decorated with the [AllowAnonymous] attribute, or
  2. neither the action nor the controller is decorated with the [Authorize] attribute.

Please note that this code assumes that the Authorize filter has not been applied globally.

There are 2 overloads of the IsAuthorized method, which validate whether a controller is authorized. The 1st overload receives an instance of the controller, a method name, and an array of parameter types.

This overload of the method applies the following logic to determine if the controller action is authorized. Either,

  1. the method is decorated with a [Authorize] attribute, or
  2. the controller is decorated with a [Authorize] attribure whilst the method is not decorated with a [AllowAnonymous] attribute.

The second overload of the IsAuthorized method add 2 parameters. An array of role names and an array of user names. This overload validates whether the controller action is authorized – as above – and then validates whether it is authorized for the roles and users.

using System;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;

namespace Framework.Tests.Mvc
{
    public static class AuthorizationTest
    {
        /// <summary>
        /// Check to see if a method allows anonymous access -
        /// 1. A method is anonymous if it is decorated with the AllowAnonymousAttribute attribute.
        /// 2. Or, a method is anonymous if neither the method nor controller are decorated with the AuthorizeAttribute attribute.
        /// </summary>
        /// <param name="controller"></param>
        /// <param name="methodName"></param>
        /// <param name="methodTypes">Optional</param>
        /// <returns>true is method is anonymous</returns>
        public static bool IsAnonymous(Controller controller, string methodName, Type[] methodTypes)
        {
            return GetMethodAttribute<AllowAnonymousAttribute>(controller, methodName, methodTypes) != null ||
                (GetControllerAttribute<AuthorizeAttribute>(controller) == null &&
                    GetMethodAttribute<AuthorizeAttribute>(controller, methodName, methodTypes) == null);

        }

        /// <summary>
        /// Check to see if a method requires authorization -
        /// 1. A method is authorized if it is decorated with the Authorize attribute.
        /// 2. Or, a method is authorized if the controller is decorated with the AuthorizeAttribute attribute, and
        /// the method is not decorated with the AllowAnonymousAttribute attribute.
        /// </summary>
        /// <param name="controller"></param>
        /// <param name="methodName"></param>
        /// <param name="methodTypes">Optional</param>
        /// <returns></returns>
        public static bool IsAuthorized(Controller controller, string methodName, Type[] methodTypes)
        {
            return GetMethodAttribute<AuthorizeAttribute>(controller, methodName, methodTypes) != null ||
                (GetControllerAttribute<AuthorizeAttribute>(controller) != null &&
                    GetMethodAttribute<AllowAnonymousAttribute>(controller, methodName, methodTypes) == null);
        }

        /// <summary>
        /// Check to see if a method requires authorization for the roles and users specified
        /// </summary>
        /// <param name="controller"></param>
        /// <param name="methodName"></param>
        /// <param name="methodTypes">Optional</param>
        /// <param name="roles"></param>
        /// <param name="users"></param>
        /// <returns></returns>
        public static bool IsAuthorized(Controller controller, string methodName, Type[] methodTypes, string[] roles, string[] users)
        {
            if (roles == null && users == null)
                return IsAuthorized(controller, methodName, methodTypes);

            if (!IsAuthorized(controller, methodName, methodTypes))
                return false;

            AuthorizeAttribute controllerAttribute = GetControllerAttribute<AuthorizeAttribute>(controller);
            AuthorizeAttribute methodAttribute = GetMethodAttribute<AuthorizeAttribute>(controller, methodName, methodTypes);

            // Check to see if all roles are authorized
            if (roles != null)
            {
                foreach (string role in roles)
                {
                    string lowerRole = role.ToLower();

                    bool roleIsAuthorized =
                        (controllerAttribute != null ?
                            controllerAttribute.Roles.ToLower().Split(',').Any(r => r == lowerRole) : false) ||
                        (methodAttribute != null ?
                            methodAttribute.Roles.ToLower().Split(',').Any(r => r == lowerRole) : false);

                    if (!roleIsAuthorized)
                        return false;
                }
            }

            // Check to see if all users are authorized
            if (users != null)
            {
                foreach (string user in users)
                {
                    string lowerUser = user.ToLower();

                    bool userIsAuthorized =
                        (controllerAttribute != null ?
                            controllerAttribute.Users.ToLower().Split(',').Any(u => u == lowerUser) : false) ||
                        (methodAttribute != null ?
                            methodAttribute.Users.Split(',').Any(u => u.ToLower() == lowerUser) : false);

                    if (!userIsAuthorized)
                        return false;
                }
            }

            return true;
        }

        private static T GetControllerAttribute<T>(Controller controller) where T : Attribute
        {
            Type type = controller.GetType();
            object[] attributes = type.GetCustomAttributes(typeof(T), true);
            T attribute = attributes.Count() == 0 ? null : (T)attributes[0];
            return attribute;
        }

        private static T GetMethodAttribute<T>(Controller controller, string methodName, Type[] methodTypes) where T : Attribute
        {
            Type type = controller.GetType();
            if (methodTypes == null)
            {
                methodTypes = new Type[0];
            }
            MethodInfo method = type.GetMethod(methodName, methodTypes);
            object[] attributes = method.GetCustomAttributes(typeof(T), true);
            T attribute = attributes.Count() == 0 ? null : (T)attributes[0];
            return attribute;
        }
    }
}

And here are some tests which utilize the AuthorizationTest class to validate whether the WidgetController actions will be correctly authorized based on the authorization attributes –

using BikeStore.Controllers;
using Framework.Tests.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Web.Mvc;

namespace BikeStore.Tests.Controllers
{
    [TestClass]
    public class WidgetControllerTests
    {
        [TestMethod]
        public void Index_IsAnonymous()
        {
            // Arrange
            WidgetController controller = new WidgetController();

            // Assert
            Assert.IsTrue(AuthorizationTest.IsAnonymous(
                controller,
                "Index",
                null));
        }

        [TestMethod]
        public void Details_IsAuthorized()
        {
            // Arrange
            WidgetController controller = new WidgetController();

            // Asset
            Assert.IsTrue(AuthorizationTest.IsAuthorized(
                controller,
                "Details",
                new Type[] { typeof(int) }));
        }

        [TestMethod]
        public void Create_Get_IsAuthorized()
        {
            // Arrange
            WidgetController controller = new WidgetController();

            // Assert
            Assert.IsTrue(AuthorizationTest.IsAuthorized(
                controller,
                "Create",
                null,
                new string[] { "Admin" },
                new string[] { "Ross" } ));
        }

        [TestMethod]
        public void Create_Post_IsAuthorized()
        {
            // Arrange
            WidgetController controller = new WidgetController();

            // Assert
            Assert.IsTrue(AuthorizationTest.IsAuthorized(
                controller,
                "Create",
                new Type[] { typeof(FormCollection) },
                new string[] { "Admin" },
                new string[] { "Ross" } ));
        }
    }
}