Extending ASP.NET MVC AuthorizeAttribute

Recently I needed to provide a custom security enforcement for an ASP.net MVC application and confirm it was working by unit testing the application without launching the browser as in an integration test. The reason for extending the AuthorizeAttribute class is that we might decide to store user credential information in a variety of differently data sources such as Active Directory, a database, an encrypted text file, etc…Or we might add custom logic to authorize a user. For the purpose of this article, I am going to simplify the authentication/authorization requirements by defining this scope:

  1. Application is an intranet application with windows authentication.

  2. User credential is to be checked against Active Directory.

  3. Authentication information is stored in web.config of application as a comma delimited string with Active Directive groups and windows user login names such as:

    <addkey=”Administrators"value=”ABC\\WebGroup1, ABC\\WebGroup1, ABC\\JaneDoe, ABC\\MaryFisher/>.If user belongs to any of the above Active Directory Groups or widows log in names access is granted, otherwise access denied.

OK, now we have set up our premises, let’s dive straight into the code for the subclass of AuthorizeAttribute:

  1. namespace SecurityDemo.Classes  
  2. {  
  3.     [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)]  
  4.     public class CustomAuthorizeAttribute: AuthorizeAttribute  
  5.     {  
  6.         public override voidOnAuthorization(AuthorizationContextfilterContext)  
  7.         {  
  8.             if (!filterContext.HttpContext.User.Identity.IsAuthenticated)  
  9.             //the user is not allowed to execute the Action. An Unauthorized result is raised.  
  10.             filterContext.Result = newHttpUnauthorizedResult();  
  11.             var roles = GetAuthorizedRoles();  
  12.             stringwindowLoginName = filterContext.HttpContext.User.Identity.Name;  
  13.             //windowLoginName and ADGroup is expected to have this format "ABC\\XYZ"  
  14.             stringdomainName = windowLoginName.Contains(@ "\") ?windowLoginName.Substring(0, windowLoginName.IndexOf(@"\  
  15.             ", System.StringComparison.OrdinalIgnoreCase)) : windowLoginName;  
  16.             windowLoginName = windowLoginName.Contains(@ "\") ? windowLoginName.Substring(windowLoginName.LastIndexOf(@ "\", System.StringComparison.OrdinalIgnoreCase) + 1): windowLoginName; boolisValidUser = false;  
  17.             if (roles.Any(role => ADClass.IsUserInADGroup(windowLoginName, role.Substring(role.LastIndexOf(@ "\", System.StringComparison.OrdinalIgnoreCase) + 1), domainName))) //if window login belongs to AD group from config  
  18.             {  
  19.                 isValidUser = true;  
  20.             }  
  21.             elseif (roles.Any(role => windowLoginName.ToLower().Equals(role.Substring(role.LastIndexOf(@ "\", System.StringComparison.OrdinalIgnoreCase) + 1).ToLower()))) //if window login belongs to a user from config  
  22.             {  
  23.                 isValidUser = true;  
  24.             }  
  25.             if (isValidUser)  
  26.             {  
  27.                 return;  
  28.             }  
  29.             else  
  30.             {  
  31.                 HandleUnauthorizedRequest(filterContext);  
  32.             }  
  33.         }  
  34.         protected override void HandleUnauthorizedRequest(AuthorizationContextfilterContext)  
  35.         {  
  36.             filterContext.Result = newViewResult  
  37.             {  
  38.                 ViewName = "~/Views/Shared/UnAuthorized.cshtml"  
  39.             };  
  40.         }  
  41.         //get list of authorized Active Directory groups and windows users from  
  42.         // web.config  
  43.         privateIEnumerable < string > GetAuthorizedRoles()  
  44.         {  
  45.             var appSettings = ConfigurationManager.AppSettings[this.Roles];  
  46.             if (string.IsNullOrEmpty(appSettings))  
  47.             {  
  48.                 return new[]  
  49.                 {  
  50.                     ""  
  51.                 };  
  52.             }  
  53.             IEnumerable < string > rolesEnumerable = appSettings.Split(',').Select(s => s.Trim());  
  54.             return rolesEnumerable;  
  55.         }  
  56.     }  
  57. }  

 

In the sublassCustomAuthorizeAttribute above we override the OnAuthorization(Authorization Context filterContext) method and provide the logic to identify the windows login user, check the person against the list of authorized Active Directory groups and Windows users from web.config. We also override against the HandleUnauthorizedRequest(AuthorizationContextfilterContext) method to return a view for access denied. Of course, as mentioned, the authorization logic can be made as flexible and complex as possible according to specific business needs.

To use the extended attribute in a controller, we just apply to attribute to a method or class as in the below code snippet:
  1. public class ProductController: Controller  
  2. {  
  3.     [CustomAuthorize(Roles = SystemRole.Administrators)]  
  4.     public ActionResultIndex()  
  5.     {  
  6.         return View("Index");  
  7.     }  
  8.     [CustomAuthorize(Roles = SystemRole.Administrators)]  
  9.     public ActionResultDetails(int Id)  
  10.     {  
  11.         return View("Details");  
  12.     }  
  13. }  
  14. // a helper class to define roles  
  15. public class SystemRole  
  16. {  
  17.     public const string Administrators = "Administrators";  
  18.     public cons tstring Sales = "Sales";  
  19. }  
There we have it, we have come up with how to implement custom security as an attribute to be applied to a controller.

Unit Testing:

We can simply test our new security feature by launching the web application through the web browser after providing the access list in the web.config as mentioned in the beginning of the article. There is nothing wrong with that. However, if we need to get more fancy and methodical by doing some full unit testing using NUnit or Microsoft UnitTestFramework (which I’ll be using in this article) then there are a few challenges we’ll be facing. First is we’ll need to simulate a browser session with a full HttpContext with widows login, session, etc… and the way to do it is to use Mock object. The second challenge is how to invoke the action methods of a controller with our CustomAuthorizeAttribute applied. The way to do it is to extend a class calledControllerActionInvoker and override a method called InvokeActionResult(). Also if you need to invoke an action method with router parameters you also need to override the GetParameterValues() method as well. Well, one picture is worth a thousand words, so I present to you a “picture” of all the code (words) involved for the unit test:
  1. namespace UnitTestSecurityDemo  
  2. {  
  3.     public class ActionInvokerExpecter < TResult > : ControllerActionInvokerwhereTResult: ActionResult  
  4.     {  
  5.         public boolIsUnAuthorized = false;  
  6.         ///<summary>  
  7.         /// override to get ViewName of controller in action  
  8.         ///</summary>  
  9.         ///<param name="controllerContext"></param>  
  10.         ///<param name="actionResult"></param>  
  11.         protected override voidInvokeActionResult(ControllerContextcontrollerContext, ActionResultactionResult)  
  12.             {  
  13.                 string viewName = ((System.Web.Mvc.ViewResult) actionResult).ViewName;  
  14.                 IsUnAuthorized = viewName.ToLower().Contains("unauthorized");  
  15.             }  
  16.             ///// <summary>  
  17.             ///// override to get Routedata of controller in action  
  18.             ///// </summary>  
  19.             ///// <param name="controllerContext"></param>  
  20.             ///// <param name="actionDescriptor"></param>  
  21.             ///// <returns></returns>  
  22.         protected overrideIDictionary < stringobject > GetParameterValues(ControllerContextcontrollerContext, ActionDescriptoractionDescriptor)  
  23.         {  
  24.             return controllerContext.RouteData.Values;  
  25.         }  
  26.     }  
  27. }  
  28. namespace UnitTestSecurityDemo  
  29. {  
  30.     [TestClass]  
  31.     public class UnitTest1  
  32.     {  
  33.         [TestMethod]  
  34.         public void TestIndexView()  
  35.         {  
  36.             var controller = new ProductController();  
  37.             MockAuthenticatedControllerContext(controller, @ "abc\jolndoe");  
  38.             ConfigurationManager.AppSettings.Set("Administrators", @ "abc\Group-ABC-App, abc\jolndoe1");  
  39.             ActionInvokerExpecter < ViewResult > a = newActionInvokerExpecter < ViewResult > ();  
  40.             a.InvokeAction(controller.ControllerContext, "Index");  
  41.             Assert.IsTrue(a.IsUnAuthorized);  
  42.         }  
  43.         [TestMethod]  
  44.         public void TestDetailsView()  
  45.         {  
  46.             //since the Details() action method of the controller has a router parameter, we need to pass  
  47.             //router data in as below  
  48.             var controller = newProductController();  
  49.             varrouteData = newRouteData();  
  50.             routeData.Values.Add("id", 3);  
  51.             MockAuthenticatedControllerContextWithRouteData(controller, @ "abc\jolndoe", routeData);  
  52.             ConfigurationManager.AppSettings.Set("Administrators", @ "abc\Group-ABC-App, abc\jolndoe");  
  53.             ActionInvokerExpecter < ViewResult > a = newActionInvokerExpecter < ViewResult > ();  
  54.             a.InvokeAction(controller.ControllerContext, "Details");  
  55.             Assert.IsTrue(a.IsUnAuthorized);  
  56.         }  
  57.         private static void MockAuthenticatedControllerContext(ProductController controller, stringuserName)  
  58.         {  
  59.             HttpContextBasehttpContext = FakeAuthenticatedHttpContext(userName);  
  60.             ControllerContext context = newControllerContext(newRequestContext(httpContext, newRouteData()), controller);  
  61.             controller.ControllerContext = context;  
  62.         }  
  63.         private static void MockAuthenticatedControllerContextWithRouteData(ProductController controller, stringuserName, RouteDatarouteData)  
  64.         {  
  65.             HttpContextBasehttpContext = FakeAuthenticatedHttpContext(userName);  
  66.             ControllerContext context = newControllerContext(newRequestContext(httpContext, routeData), controller);  
  67.             controller.ControllerContext = context;  
  68.         }  
  69.         public static HttpContextBaseFakeAuthenticatedHttpContext(string username)  
  70.         {  
  71.             Mock < HttpContextBase > context = newMock < HttpContextBase > ();  
  72.             Mock < HttpRequestBase > request = newMock < HttpRequestBase > ();  
  73.             Mock < HttpResponseBase > response = newMock < HttpResponseBase > ();  
  74.             Mock < HttpSessionStateBase > session = newMock < HttpSessionStateBase > ();  
  75.             Mock < HttpServerUtilityBase > server = newMock < HttpServerUtilityBase > ();  
  76.             Mock < IPrincipal > user = newMock < IPrincipal > ();  
  77.             Mock < IIdentity > identity = newMock < IIdentity > ();  
  78.             context.Setup(ctx => ctx.Request).Returns(request.Object);  
  79.             context.Setup(ctx => ctx.Response).Returns(response.Object);  
  80.             context.Setup(ctx => ctx.Session).Returns(session.Object);  
  81.             context.Setup(ctx => ctx.Server).Returns(server.Object);  
  82.             context.Setup(ctx => ctx.User).Returns(user.Object);  
  83.             user.Setup(ctx => ctx.Identity).Returns(identity.Object);  
  84.             identity.Setup(id => id.IsAuthenticated).Returns(true);  
  85.             identity.Setup(id => id.Name).Returns(username);  
  86.             returncontext.Object;  
  87.         }  
  88.     }  
  89. }  
Full source code is provided with this article.