In my continual quest for find better ways of doing anything and everything this week I decided to tackle on of my arch nemesis: Security in ASP.Net, specifically Security in ASP.Net MVC3. You might ask why? We all know that there are at least 2 easy ways to implement security in MVC3

  1. Authorize Attributes on Controllers and Actions
  2. web.config locations

Yuck! I hate both of them for the same reason: they become very difficult to manage. Authorize Attributes end up spread all over your application and web.config location elements require 7 lines of xml for 1 rule. So I went searching and came across a NuGet package called FluentSecurity.

The documentation covered all the examples I needed and you can define rules in only 1 line of code!


configuration.For.RequireRole("Administrator", "Legal");

I ran into 2 difficulties when trying to implement this:

  1. Implementing an Access Denied page and re-direct to Login
  2. Integrating FluentSecurity with MvcSiteMapProvider

Implementing an Access Denied page and re-direct to Login

The current NuGet version (1.4 as at 05/2012) required that you use StructureMap or another IoC container to implement a custom IPolicyViolationHandler. I wasn’t using either but discovered that it is not a requirement in version 2 which is in alpha at the time of writing but works fine for me. So make sure either the Version you get from NuGet is at least 2.0 or got to GitHub to get the latest version.

First we need to create a class implementing IPolicyViolationHandler.
The class that we create will:

  • Send unauthenticated users to a shared view called AccessDenied
  • Redirect authenticated users without access to the current controller to the Account controllers LogIn action

using System.Web.Mvc;
using System.Web.Routing;
using FluentSecurity;

public class DefaultPolicyViolationHandler : IPolicyViolationHandler
{
  public string ViewName = "AccessDenied";

  public ActionResult Handle(PolicyViolationException exception)
  {
    if (SecurityHelper.UserIsAuthenticated())
    {
      return new ViewResult { ViewName = ViewName };
    }
    else
    {
      RouteValueDictionary rvd = new RouteValueDictionary();

      if (System.Web.HttpContext.Current.Request.RawUrl != "/")
        rvd["ReturnUrl"] = System.Web.HttpContext.Current.Request.RawUrl;

      rvd["controller"] = "Account";
      rvd["action"] = "LogOn";
      rvd["area"] = "";

      return new RedirectToRouteResult(rvd);
    }
  }
}

Now we need to set / tell FluentSecurity to use our DefaultPolicyViolationHandler. For this I created a SecurityHelper class.


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

using FluentSecurity;
using FluentSecurityAndMvcSiteMapProvider.Areas.Admin.Controllers;
using FluentSecurityAndMvcSiteMapProvider.Controllers;

public static class SecurityHelper
{
  public static ISecurityConfiguration SetupFluentSecurity()
  {
    SecurityConfigurator.Configure(configuration =>
    {
    // Let Fluent Security know how to get the authentication status of the current user
      configuration.GetAuthenticationStatusFrom(SecurityHelper.UserIsAuthenticated);
      configuration.GetRolesFrom(SecurityHelper.UserRoles);

      // It is reccommended not to use this setting. For all of our applications users must always be authenticated.
      configuration.IgnoreMissingConfiguration();

      configuration.DefaultPolicyViolationHandlerIs(() => new DefaultPolicyViolationHandler());

      //Make sure user must be authenticated but allow unauthenticated access to the logon screen
      configuration.ForAllControllers().DenyAnonymousAccess();
      configuration.For<AccountController>(ac =&gt; ac.LogOn()).Ignore();

      //first deny access to all users except Administrators
      configuration.ForAllControllersInNamespaceContainingType<AdminController>()
      .DenyAuthenticatedAccess()
      .RequireRole(RolesEnum.AdminRole.ToString());

      //If any users have access to part of the admin console they will access to the admin dashboard
      configuration.For<AdminController>().RequireRole(RolesEnum.AdminRole.ToString(), RolesEnum.LegalRole.ToString());

      //grant access to any other controllers in the admin area
      configuration.For<LegalController>().RequireRole(RolesEnum.AdminRole.ToString(), RolesEnum.LegalRole.ToString());

    });

    return SecurityConfiguration.Current;
  }

  public static bool UserIsAuthenticated()
  {
    var currentUser = HttpContext.Current.User;
    return !string.IsNullOrEmpty(currentUser.Identity.Name);
  }

  public static IEnumerable<object> UserRoles()
  {
    var currentUser = HttpContext.Current.User;
    return string.IsNullOrEmpty(currentUser.Identity.Name) ? null : System.Web.Security.Roles.GetRolesForUser(currentUser.Identity.Name);
  }
}

Finally configure our system to use FluentSecurity in Global.asax.cs


using System.Web.Mvc;
using System.Web.Routing;

using FluentSecurity;

namespace FluentSecurityAndMvcSiteMapProvider
{
  // Note: For instructions on enabling IIS6 or IIS7 classic mode,
  // visit http://go.microsoft.com/?LinkId=9394801

  public class MvcApplication : System.Web.HttpApplication
  {
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
      filters.Add(new HandleErrorAttribute());

      //required for FluentSecurity
      filters.Add(new HandleSecurityAttribute(), 0);
    }

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      routes.MapRoute(
      "Default", // Route name
      "{controller}/{action}/{id}", // URL with parameters
      new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
      );
    }

    protected void Application_Start()
    {
    //Configure FluentSecurity
      SecurityHelper.SetupFluentSecurity();

      AreaRegistration.RegisterAllAreas();

      RegisterGlobalFilters(GlobalFilters.Filters);
      RegisterRoutes(RouteTable.Routes);
    }
  }
}

Integrating FluentSecurity with MvcSiteMapProvider

With security working I needed to filter my site map so unauthenticated users can’t see menu items they shouldn’t. Becase we love to user MvcSiteMapProvider I could have easily just added a role attribute to each mvcSiteMapNode but then we are doubling up on security definitions and I just can’t have that.

To hook into MvcSiteMapProvier and use our own Visibility provider we needed to do a few things

  • Create a new function in the SecurityHelper to check if a user has access to a particular action
  • Create a new class implementing ISiteMapNodeVisibilityProvider

Creating ActionIsAllowedForUser. The one thing that is important to note about this function is that GetContainerFor asks for a parameter named controllerName. This is actually after the controllerNamespace.


public static class SecurityHelper
{
  public static ISecurityConfiguration SetupFluentSecurity()
  {
    //...
  }

  public static bool UserIsAuthenticated()
  {
    //...
  }

  public static IEnumerable<object> UserRoles()
  {
    /...
  }

  public static bool ActionIsAllowedForUser(string controllerNamespace, string actionName)
  {
    var configuration = SecurityConfiguration.Current;

    var policyContainer = configuration.PolicyContainers.GetContainerFor(controllerNamespace, actionName);
    if (policyContainer != null)
    {
      var context = SecurityContext.Current;
      var results = policyContainer.EnforcePolicies(context);
      return results.All(x =&gt; x.ViolationOccured == false);
    }
  return true;
}

Now we can implement a SiteMapNodeVisibilityProvider to call ActionIsAllowedForUser. Lucky MvcSiteMapProvider provides us with this wonderful function that will get the full controller namespace for us!


using System.Collections.Generic;
using System.Web;

using MvcSiteMapProvider;
using MvcSiteMapProvider.Extensibility;

public class ReviumVisibilityProvider : ISiteMapNodeVisibilityProvider
{
  public bool IsVisible(SiteMapNode node, HttpContext context, IDictionary sourceMetadata)
  {
    // Convert to MvcSiteMapNode
    var mvcNode = node as MvcSiteMapNode;
    if (mvcNode == null)
    {
      return true;
    }

    //First check the visibility based on user roles
    string controllerNamespace = (new DefaultControllerTypeResolver()).ResolveControllerType(mvcNode.Area, mvcNode.Controller).FullName;
    bool isVisible = SecurityHelper.ActionIsAllowedForUser(controllerNamespace, mvcNode.Action);
    if (!isVisible)
      return false;

    //Process Other Visibility Rules
    //...

    return true;
  }
}

And that’s it! We now have a site that full implements FluentSecurity for validation, displays the appropriate Access denied or login page and displays the correct site map foe each user.

You may also like

Creating a generic settings repository in C#

Every project has a group of settings that can be used to configure a web site. They can be stored in the web.config file or in a database. Typically what happens is you create a number of functions to read settings like...

Keep Reading

ASP.NET MVC3 – Application_Error not firing log4net

Recently we have started developing a number of MVC3 applications. They have all started to be released to our staging and production servers and for some reason we are no longer seeing the error emails / log. The best explanation I found for this was on stack overflow.

Keep Reading

Newsletter sign up

Every couple of months we send out an update on what's been happening around our office and the web. Sign up and see what you think. And of course, we never spam.