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)
- //the user is not allowed to execute the Action. An Unauthorized result is raised.
- filterContext.Result = newHttpUnauthorizedResult();
- var roles = GetAuthorizedRoles();
- stringwindowLoginName = filterContext.HttpContext.User.Identity.Name;
- //windowLoginName and ADGroup is expected to have this format "ABC\\XYZ"
- 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))) //if window login belongs to AD group from config
- {
- isValidUser = true;
- }
- 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
- {
- isValidUser = true;
- }
- if (isValidUser)
- {
- return;
- }
- else
- {
- HandleUnauthorizedRequest(filterContext);
- }
- }
- protected override void HandleUnauthorizedRequest(AuthorizationContextfilterContext)
- {
- filterContext.Result = newViewResult
- {
- ViewName = "~/Views/Shared/UnAuthorized.cshtml"
- };
- }
- //get list of authorized Active Directory groups and windows users from
- // web.config
- 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");
- }
- }
- // a helper class to define roles
- public class SystemRole
- {
- public const string Administrators = "Administrators";
- public cons tstring Sales = "Sales";
- }
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;
- ///<summary>
- /// override to get ViewName of controller in action
- ///</summary>
- ///<param name="controllerContext"></param>
- ///<param name="actionResult"></param>
- protected override voidInvokeActionResult(ControllerContextcontrollerContext, ActionResultactionResult)
- {
- string viewName = ((System.Web.Mvc.ViewResult) actionResult).ViewName;
- IsUnAuthorized = viewName.ToLower().Contains("unauthorized");
- }
- ///// <summary>
- ///// override to get Routedata of controller in action
- ///// </summary>
- ///// <param name="controllerContext"></param>
- ///// <param name="actionDescriptor"></param>
- ///// <returns></returns>
- 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()
- {
- //since the Details() action method of the controller has a router parameter, we need to pass
- //router data in as below
- 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;
- }
- }
- }
Arul RPosted Jan 27, 2016, 8:39 AM
Nice
Raja TPosted Jan 27, 2016, 8:05 AM
Nice, Thanks for sharing
Ankit BansalPosted Jan 27, 2016, 6:24 AM
Nice share..thanks..
Mohammed IbrahimPosted Jan 27, 2016, 3:22 AM
nice
Debasis SahaPosted Jan 26, 2016, 9:11 AM
Good One..
Mithun PattankarPosted Jan 26, 2016, 8:51 AM
Well article is very good but it's not part of ASP.NET Core. This is existing ASP.NET MVC 5
Ehsan SajjadPosted Jan 26, 2016, 7:10 AM
Good one
Mohammed IbrahimPosted Jan 26, 2016, 6:49 AM
nice
Tapan DubeyPosted Jan 26, 2016, 4:58 AM
Nice Article thanks for sharing...
Shashangka ShekharPosted Jan 26, 2016, 4:56 AM
Nice
Muhammad Aqib ShehzadPosted Jan 26, 2016, 4:39 AM
fabulous article
Bhavik PatelPosted Jan 26, 2016, 3:36 AM
good article... well explained
Jaco ZwartsPosted Jan 26, 2016, 2:28 AM
Nice Article