Dynamic Controller/Action Authorization in ASP.NET MVC

Note: This has a CodePlex project now: http://mvcauthorization.codeplex.com/. The code there is a little different than the code below, and has some nice new features. The code below should still serve as a good reference, however.

Often times, after you’ve authenticated your users, you’ll want to authorize what they actually have control over based on role. Typically in an ASP.NET MVC project this happens by using an authorize attribute not unlike the one shown below:

[Authorize(Roles = "Programmer, Manager")]
public ActionResult MyTopSecretActionForSuperCoolPeopleOnly()

Unfortunately, the above code directly ties your action and controller code to your user roles. So what happens if a new role is added? Well, in my fake system I just added one, and it’s called the Fonzie role. Anyone who is at Fonzie status is by definition super cool, and needs to have access to our super cool action. So our code above would become:

[Authorize(Roles = "Programmer, Manager, Fonzie")]
public ActionResult MyTopSecretActionForSuperCoolPeopleOnly()

But, because our solution is code based we need to wait for a re-deployment and only then will the Fonzies get access to to our action. Not good! Also, what do we do about any action links to that action? We don’t want them just showing up for any old user. We’d better address that as well.

The solution to this problem is to create new configuration sections in web.config, which will allow us to specify authorization roles for our actions and controllers in a declarative way. That way, we only need to update the configuration, and the roles are automatically mapped to the actions and controllers without need for re-deployment. Below is what the code using this functionality should ultimately look like.

Define a configuration for controller/action roles:

  <authorizationConfiguration>
    <controllerAuthorizationMappings>
      <add controller="Home" role="GeneralAccess">        
        <!-- Define allowed roles for actions under the Home Controller -->
        <actionAuthorizationMappings>
          <add action="MyTopSecretActionForSuperCoolPeopleOnly" roles="Developer,Manager,Fonzie" />
        </actionAuthorizationMappings>
      </add>
    </controllerAuthorizationMappings>
  </authorizationConfiguration>

The configuration above says we should allow any user with GeneralAccess to the “Home” controller, but our top secret action should only be accessible to users with a Developer, Manager, or Fonzie role. Then to use it, we would just need to decorate our action with a new attribute called ActionAuthorize, which is derived from AuthorizeAttribute, and needs no parameters:

[ActionAuthorize]
public ActionResult MyTopSecretActionForSuperCoolPeopleOnly()

And the new attribute would be smart enough to look-up the roles for this action and allow/disallow access based on the configuration. (Note that in addition to [ActionAuthorize], we also have an attribute called [ControllerAuthorize] which does the same thing for controllers).

When we link to this action, we would need to have a custom html helper that only renders the action link when the user is in role for that action. The usage would look like:

<%= Html.SecuredActionLink("Secure Link.", "MyTopSecretActionForSuperCoolPeopleOnly") %>

To accomplish the above we will need to start off with five classes for our configuration section:

AuthorizationConfiguration
This class defines the root or our configuration section and contains a static accessor property, as well as a ControllerAuthorizationConfigurationCollection.

    public class AuthorizationConfiguration : ConfigurationSection
    {
        private static AuthorizationConfiguration _authorizationConfiguration
            = ConfigurationManager.GetSection("authorizationConfiguration") as AuthorizationConfiguration;

        public static AuthorizationConfiguration Section
        {
            get
            {
                return _authorizationConfiguration;
            }
        }

        [ConfigurationProperty("controllerAuthorizationMappings")]
        public ControllerAuthorizationConfigurationCollection ControllerAuthorizationMappings
        {
            get
            {
                return this["controllerAuthorizationMappings"] as ControllerAuthorizationConfigurationCollection;
            }
        }
    }

ControllerAuthorizationConfigurationCollection
This class will encapsulate our collection of Controller configurations.

   public class ControllerAuthorizationConfigurationCollection : ConfigurationElementCollection, 

IEnumerable<ControllerAuthorizationConfigurationElement>
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public ControllerAuthorizationConfigurationElement this[int index]
        {
            get
            {
                return base.BaseGet(index) as ControllerAuthorizationConfigurationElement;
            }
            set
            {
                if (base.BaseGet(index) != null)
                {
                    base.BaseRemoveAt(index);
                }
                this.BaseAdd(index, value);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public IEnumerable<ControllerAuthorizationConfigurationElement> Elements
        {
            get
            {
                for (int i = 0; i < base.Count; ++i)
                {
                    yield return this[i];
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public new IEnumerator<ControllerAuthorizationConfigurationElement> GetEnumerator()
        {
            for (int i = 0; i < base.Count; ++i)
            {
                yield return this[i];
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected override ConfigurationElement CreateNewElement()
        {
            return new ControllerAuthorizationConfigurationElement();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((ControllerAuthorizationConfigurationElement)element).Controller;
        } 
    }

ControllerAuthorizationConfigurationElement

This class will define the base configuration element for a controller. It has an optional property called “Roles” which we can use to lock down a controller by a certain set of roles. It also has an actionAuthorizationConfigurationCollection to maintain a list of actions.

    public class ControllerAuthorizationConfigurationElement : ConfigurationElement
    {
        [ConfigurationProperty("controller", IsRequired = true)]
        public string Controller
        {
            get
            {
                return (string)this["controller"];
            }
            set
            {
                this["controller"] = value;
            }
        }

        [ConfigurationProperty("roles", IsRequired = false)]
        public string Roles
        {
            get
            {
                return (string)this["roles"];
            }
            set
            {
                this["roles"] = value;
            }
        }

        [ConfigurationProperty("actionAuthorizationMappings")]
        public ActionAuthorizationConfigurationCollection ActionAuthorizationMappings
        {
            get
            {
                return this["actionAuthorizationMappings"] as ActionAuthorizationConfigurationCollection;
            }
        }
    }

ActionAuthorizationConfigurationCollection
This class will encapsulate our collection of Controller configurations under the current controller.

   public class ActionAuthorizationConfigurationCollection : ConfigurationElementCollection, 

IEnumerable<ActionAuthorizationConfigurationElement>
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public ActionAuthorizationConfigurationElement this[int index]
        {
            get
            {
                return base.BaseGet(index) as ActionAuthorizationConfigurationElement;
            }
            set
            {
                if (base.BaseGet(index) != null)
                {
                    base.BaseRemoveAt(index);
                }
                this.BaseAdd(index, value);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public IEnumerable<ActionAuthorizationConfigurationElement> Elements
        {
            get
            {
                for (int i = 0; i < base.Count; ++i)
                {
                    yield return this[i];
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public new IEnumerator<ActionAuthorizationConfigurationElement> GetEnumerator()
        {
            for (int i = 0; i < base.Count; ++i)
            {
                yield return this[i];
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected override ConfigurationElement CreateNewElement()
        {
            return new ActionAuthorizationConfigurationElement();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((ActionAuthorizationConfigurationElement)element).Action;
        } 
    }

ActionAuthorizationConfigurationElement
This class will encapsulate a particular action under a controller. It has a required “Roles” parameter.

    public class ActionAuthorizationConfigurationElement : ConfigurationElement
    {
        [ConfigurationProperty("action", IsRequired = true)]
        public string Action
        {
            get
            {
                return (string)this["action"];
            }
            set
            {
                this["action"] = value;
            }
        }

        [ConfigurationProperty("roles", IsRequired = true)]
        public string Roles
        {
            get
            {
                return (string)this["roles"];
            }
            set
            {
                this["roles"] = value;
            }
        }
    }

After we have defined our configuration elements and registered the section in our web.config, we can start using it. We define two attribute classes which will perform the authentication look-up for our controllers and actions:

ActionAuthorizeAttribute targets an action method:

    /// <summary>
    /// Authorization attribute for an MVC action method
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public class ActionAuthorizeAttribute : AuthorizeAttribute
    {
        private string[] _rolesArray;
        private string _authorizationGroupName;

        /// <summary>
        /// 
        /// </summary>
        public string AccessDeniedController { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string AccessDeniedAction { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public ActionAuthorizeAttribute()
        {
            AccessDeniedController = ConfigurationManager.AppSettings["DefaultAccessDeniedController"] ?? 

"AccessDenied";
            AccessDeniedAction = ConfigurationManager.AppSettings["DefaultAccessDeniedAction"] ?? "Index";
            _authorizationGroupName = string.Empty;
        }

        /// <summary>
        /// 
        /// </summary>
        public string[] RolesArray
        {
            get
            {
                return _rolesArray;
            }
            set
            {
                _rolesArray = value;
                this.Roles = string.Join(",", _rolesArray);
            }
        }

        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;

            // Get any controller specific role mappings
            var controllerRoleMappings = AuthorizationConfiguration.Section.ControllerAuthorizationMappings.Where(e 

=> e.Controller == controllerName).FirstOrDefault();

            if (controllerRoleMappings != null)
            {
                var actionRoleMappings = controllerRoleMappings.ActionAuthorizationMappings.Where(e => e.Action == 

filterContext.ActionDescriptor.ActionName).FirstOrDefault();

                if (actionRoleMappings != null && !string.IsNullOrEmpty(actionRoleMappings.Roles))
                {
                    this.RolesArray = actionRoleMappings.Roles.Split(',');
                }
            }

            //
            base.OnAuthorization(filterContext);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { 

Controller = AccessDeniedController, Action = AccessDeniedAction, Roles = Roles }));
        }
    }

ControllerAuthorizeAttribute targets a controller class:

    /// <summary>
    /// Authorization attribute for an MVC controller
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class ControllerAuthorizeAttribute : AuthorizeAttribute
    {
        private string[] _rolesArray;
        private string _authorizationGroupName;

        /// <summary>
        /// 
        /// </summary>
        public string AccessDeniedController { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string AccessDeniedAction { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public ControllerAuthorizeAttribute()
        {
            AccessDeniedController = ConfigurationManager.AppSettings["DefaultAccessDeniedController"] ?? 

"AccessDenied";
            AccessDeniedAction = ConfigurationManager.AppSettings["DefaultAccessDeniedAction"] ?? "Index";
            _authorizationGroupName = string.Empty;
        }

        /// <summary>
        /// 
        /// </summary>
        public string[] RolesArray
        {
            get
            {
                return _rolesArray;
            }
            set
            {
                _rolesArray = value;
                this.Roles = string.Join(",", _rolesArray);
            }
        }

        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;

            // Get any controller specific role mappings
            var controllerRoleMappings = AuthorizationConfiguration.Section.ControllerAuthorizationMappings.Where(e 

=> e.Controller == controllerName).FirstOrDefault();

            if (controllerRoleMappings != null && !string.IsNullOrEmpty(controllerRoleMappings.Roles))
            {
                this.RolesArray = controllerRoleMappings.Roles.Split(',');
            }

            //
            base.OnAuthorization(filterContext);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { 

Controller = AccessDeniedController, Action = AccessDeniedAction, Roles = Roles }));
        }
    }

Then for dynamic action link rendering based on role, the following Html helper would be as follows:

        /// <summary>
        /// Renders the action link, provided the user has access to the action method being linked to
        /// </summary>
        /// <param name="helper"></param>
        /// <param name="linkText"></param>
        /// <param name="actionName"></param>
        /// <returns></returns>
        public static MvcHtmlString SecuredActionLink(this HtmlHelper helper, string linkText, string actionName)
        {
            MvcHtmlString html = MvcHtmlString.Empty;
            string[] roles = null;

            RouteData routeData = helper.ViewContext.RouteData;
            string controllerName = routeData.GetRequiredString("controller");

            // Get any controller specific role mappings
            var controllerRoleMappings = AuthorizationConfiguration.Section.ControllerAuthorizationMappings.Where(e 

=> e.Controller == controllerName).FirstOrDefault();
            ActionAuthorizationConfigurationElement actionRoleMappings = null;

            if (controllerRoleMappings != null)
            {
                actionRoleMappings = controllerRoleMappings.ActionAuthorizationMappings.Where(e => e.Action == 

actionName).FirstOrDefault();

                if (actionRoleMappings != null && !string.IsNullOrEmpty(actionRoleMappings.Roles))
                {
                    roles = actionRoleMappings.Roles.Split(',');
                }
            }

            if (roles != null)
            {
                foreach (string role in roles)
                {
                    if (ApplicationUser.Current.Roles.Contains(role))
                    {
                        html = System.Web.Mvc.Html.LinkExtensions.ActionLink(helper, linkText, actionName);
                        break;
                    }
                }
            }
            else if (actionRoleMappings == null)
            {
                // If there's no roles associated to that action then render the link
                html = System.Web.Mvc.Html.LinkExtensions.ActionLink(helper, linkText, actionName);
            }

            return html;
        }
    }

The helper above will figure out if the user has access to the action, display accordingly, and viola! Users will never see a link to an action for which they do not have access, and if they do somehow get to an action that is restricted based on their role they will get redirected to an access denied page of your choosing.

This is just a basic sample, but there’s lots that can be done in addition to this. Be sure to download the source here to see it in action (VS 2010, ASP.NET MVC 2, and WIF required). In particular play around with the role definitions at the bottom of the web.config to see the actions dynamically authorize as the roles in the config change. The authentication is done via the default STS server project that gets created when you create a federated security application, so you will need to install the WIF RTW and SDK in order to successfully run the project the way it is.

24 thoughts on “Dynamic Controller/Action Authorization in ASP.NET MVC

  1. Pingback: Tweets that mention Ryan's Blog

  2. Pingback: Ryanmwright

  3. Jigar

    Hi Ryan,

    Can you please explain How can we do this thing with db? I have tables like controller, action, roles, users, userhasroles, rolehasaction.

    Thanks,

    Jigar.

  4. Ryan Post author

    @Jigar
    Sure.

    In the ControllerAuthorizeAttribute and ActionAuthorizeAttribute classes there’s an override to OnAuthorization. Currently this override get’s the controller/action name and looks up it’s corresponding roles from the configuration. If you remove the configuration lookup and replace it with a database lookup you should be good to go. You can also get rid of the ConfigurationElement declarations as well, since the database will be the backing store.

  5. Pingback: Codeplex Project for Dynamic Controller/Action Authorization in ASP.NET MVC | Ryan's Blog

  6. dodat

    could U please to up it again, i search ur link at blog but i can’t find source code on it
    thanks a lot and have a nice working day!

  7. Chris Nevill

    I’ve downloaded the Codeplex project. However it seems quite different to this article. There’s no actionauthorize attributes in place.

    Could you put some simple documentation together on how to work with the codeplex project?

  8. Mahmoud Ali

    Great Job, Thank you.

    Is there a way i can maintain this permissions on actions/controllers in runtime through the code?

  9. Ryan Post author

    Sure,

    You need to create a class that overrides the AuthorizationProvider base class, and implement the functionality that loads the action/controller/etc metadata from whatever place you want to store it in. Then in your global.asax file you need to tell MvcAuthorization how to resolve your implementation: AuthorizationProvider.ResolveDependenciesUsing(type => YOUR CODE HERE);

    Please note though that the data you return is cached (typically for the lifetime of the AppDomain if you are using the default configuration).


    public class CustomAuthorizationProvider : AuthorizationProvider
    {
    protected override GlobalAuthorizationDescriptor LoadGlobalAuthorizationDescriptor()
    {
    ...
    }

    protected override AreaAuthorizationDescriptor LoadAreaAuthorizationDescriptor(string areaName)
    {
    ...
    }

    protected override ControllerAuthorizationDescriptor LoadControllerAuthorizationDescriptor(string controllerName, string areaName)
    {
    ...
    }

    protected override ActionAuthorizationDescriptor LoadActionAuthorizationDescriptor(string controllerName, string actionName, string areaName)
    {
    ...
    }

    protected IEnumerable LoadPolicyHandlerFromConfig(PolicyAuthorizationConfigurationCollection policyHandlerCollection)
    {
    ...
    }
    }

  10. Mahmoud Ali

    Rayan,

    Sorry but i am a bit new to MVC so can you help me with the
    AuthorizationProvider.ResolveDependenciesUsing(type => YOUR CODE HERE);

    How can i tell it how to resolve my implementation?

  11. Mahmoud Ali

    How can i set the access denied Controller/action i tried to use the same configuration you implemented in the old version but it didnt work out, is there a replacement for it?

  12. Ryan Post author

    Hi,

    I have updated the code and submitted an updated nuget package. There are three properties on the authorize filter that will give the desired functionality.


    new AuthorizeFilter() { AccessDeniedAction = "About", AccessDeniedController = "Home", AccessDeniedArea = "MyArea" }

  13. Mahmoud Ali

    Hey Rayan,

    Thanks for adding the access denied controller/action but can you please let me know where should i set it? right now i have my custom authorization provider where i get my permissions from database.

  14. Jason

    This would be a great reference, but it’s a pity that you don’t offer this for download and just point everyone to the other version – that one is okay but it’s definitely overkill for what a lot of people need. I personally just need Controller and Action authorization and while I managed to get the values read from the web.config I can’t seem to get this to work…

Leave a Reply

Your email address will not be published. Required fields are marked *