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:
- Application is an intranet application with windows authentication.
- User credential is to be checked against Active Directory.
- 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:
- namespace SecurityDemo.Classes
- {
- [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)]
- public class CustomAuthorizeAttribute: AuthorizeAttribute
- {
- public override voidOnAuthorization(AuthorizationContextfilterContext)
- {
- if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
-
- filterContext.Result = newHttpUnauthorizedResult();
- var roles = GetAuthorizedRoles();
- stringwindowLoginName = filterContext.HttpContext.User.Identity.Name;
-
- stringdomainName = windowLoginName.Contains(@ "\") ?windowLoginName.Substring(0, windowLoginName.IndexOf(@"\
- ", System.StringComparison.OrdinalIgnoreCase)) : windowLoginName;
- windowLoginName = windowLoginName.Contains(@ "\") ? windowLoginName.Substring(windowLoginName.LastIndexOf(@ "\", System.StringComparison.OrdinalIgnoreCase) + 1): windowLoginName; boolisValidUser = false;
- if (roles.Any(role => ADClass.IsUserInADGroup(windowLoginName, role.Substring(role.LastIndexOf(@ "\", System.StringComparison.OrdinalIgnoreCase) + 1), domainName)))
- {
- isValidUser = true;
- }
- elseif (roles.Any(role => windowLoginName.ToLower().Equals(role.Substring(role.LastIndexOf(@ "\", System.StringComparison.OrdinalIgnoreCase) + 1).ToLower())))
- {
- isValidUser = true;
- }
- if (isValidUser)
- {
- return;
- }
- else
- {
- HandleUnauthorizedRequest(filterContext);
- }
- }
- protected override void HandleUnauthorizedRequest(AuthorizationContextfilterContext)
- {
- filterContext.Result = newViewResult
- {
- ViewName = "~/Views/Shared/UnAuthorized.cshtml"
- };
- }
-
-
- privateIEnumerable < string > GetAuthorizedRoles()
- {
- var appSettings = ConfigurationManager.AppSettings[this.Roles];
- if (string.IsNullOrEmpty(appSettings))
- {
- return new[]
- {
- ""
- };
- }
- IEnumerable < string > rolesEnumerable = appSettings.Split(',').Select(s => s.Trim());
- return rolesEnumerable;
- }
- }
- }
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:
- public class ProductController: Controller
- {
- [CustomAuthorize(Roles = SystemRole.Administrators)]
- public ActionResultIndex()
- {
- return View("Index");
- }
- [CustomAuthorize(Roles = SystemRole.Administrators)]
- public ActionResultDetails(int Id)
- {
- return View("Details");
- }
- }
-
- public class SystemRole
- {
- public const string Administrators = "Administrators";
- public cons tstring Sales = "Sales";
- }
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:
- namespace UnitTestSecurityDemo
- {
- public class ActionInvokerExpecter < TResult > : ControllerActionInvokerwhereTResult: ActionResult
- {
- public boolIsUnAuthorized = false;
-
-
-
-
-
- protected override voidInvokeActionResult(ControllerContextcontrollerContext, ActionResultactionResult)
- {
- string viewName = ((System.Web.Mvc.ViewResult) actionResult).ViewName;
- IsUnAuthorized = viewName.ToLower().Contains("unauthorized");
- }
-
-
-
-
-
-
- protected overrideIDictionary < string, object > GetParameterValues(ControllerContextcontrollerContext, ActionDescriptoractionDescriptor)
- {
- return controllerContext.RouteData.Values;
- }
- }
- }
- namespace UnitTestSecurityDemo
- {
- [TestClass]
- public class UnitTest1
- {
- [TestMethod]
- public void TestIndexView()
- {
- var controller = new ProductController();
- MockAuthenticatedControllerContext(controller, @ "abc\jolndoe");
- ConfigurationManager.AppSettings.Set("Administrators", @ "abc\Group-ABC-App, abc\jolndoe1");
- ActionInvokerExpecter < ViewResult > a = newActionInvokerExpecter < ViewResult > ();
- a.InvokeAction(controller.ControllerContext, "Index");
- Assert.IsTrue(a.IsUnAuthorized);
- }
- [TestMethod]
- public void TestDetailsView()
- {
-
-
- var controller = newProductController();
- varrouteData = newRouteData();
- routeData.Values.Add("id", 3);
- MockAuthenticatedControllerContextWithRouteData(controller, @ "abc\jolndoe", routeData);
- ConfigurationManager.AppSettings.Set("Administrators", @ "abc\Group-ABC-App, abc\jolndoe");
- ActionInvokerExpecter < ViewResult > a = newActionInvokerExpecter < ViewResult > ();
- a.InvokeAction(controller.ControllerContext, "Details");
- Assert.IsTrue(a.IsUnAuthorized);
- }
- private static void MockAuthenticatedControllerContext(ProductController controller, stringuserName)
- {
- HttpContextBasehttpContext = FakeAuthenticatedHttpContext(userName);
- ControllerContext context = newControllerContext(newRequestContext(httpContext, newRouteData()), controller);
- controller.ControllerContext = context;
- }
- private static void MockAuthenticatedControllerContextWithRouteData(ProductController controller, stringuserName, RouteDatarouteData)
- {
- HttpContextBasehttpContext = FakeAuthenticatedHttpContext(userName);
- ControllerContext context = newControllerContext(newRequestContext(httpContext, routeData), controller);
- controller.ControllerContext = context;
- }
- public static HttpContextBaseFakeAuthenticatedHttpContext(string username)
- {
- Mock < HttpContextBase > context = newMock < HttpContextBase > ();
- Mock < HttpRequestBase > request = newMock < HttpRequestBase > ();
- Mock < HttpResponseBase > response = newMock < HttpResponseBase > ();
- Mock < HttpSessionStateBase > session = newMock < HttpSessionStateBase > ();
- Mock < HttpServerUtilityBase > server = newMock < HttpServerUtilityBase > ();
- Mock < IPrincipal > user = newMock < IPrincipal > ();
- Mock < IIdentity > identity = newMock < IIdentity > ();
- context.Setup(ctx => ctx.Request).Returns(request.Object);
- context.Setup(ctx => ctx.Response).Returns(response.Object);
- context.Setup(ctx => ctx.Session).Returns(session.Object);
- context.Setup(ctx => ctx.Server).Returns(server.Object);
- context.Setup(ctx => ctx.User).Returns(user.Object);
- user.Setup(ctx => ctx.Identity).Returns(identity.Object);
- identity.Setup(id => id.IsAuthenticated).Returns(true);
- identity.Setup(id => id.Name).Returns(username);
- returncontext.Object;
- }
- }
- }
Full source code is provided with this article.