Category Archives: ASP.NET MVC

ASP.NET MVC Development

Codeplex Project for Dynamic Controller/Action Authorization in ASP.NET MVC

For those of you who used the code in my post here: http://www.ryanmwright.com/2010/04/25/dynamic-controlleraction-authorization-in-asp-net-mvc/

I have created a project on Codeplex for it (http://mvcauthorization.codeplex.com/). Note that this version has some major breaking changes from the code I posted in my previous post but has some major improvements, the two biggest being:

  • Provider based model so that a database provider is as simple as overriding two methods on AuthorizationProvider
  • A global action filter for authorization (requires DependencyResolver)

Check it out and let me know what you think. I will be adding a post to show how to integrate it with the MvcSitemap provider soon.

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.