Sunday, September 06, 2009 4:06 PM Kazi Manzur Rashid

ASP.NET MVC and Authorization and Monkey Patching

[Update: Maarten Balliauw confirmed that he has applied the suggested fix in MVC Sitemap provider]

As you know that we will be including Menu in our final release, when defining the menu, it will allow to specify the Route Name, Controller/Action name and associated route values for an menu item so that we can generate the corresponding url. One of the basic feature that we want to include is, when rendering the Menu it will scan through the controller’s actions and only render the menu items that the currently visiting user has permission. As you can guess, it is related with the AuthorizeAttribute of ASP.NET MVC framework. The actual method that is responsible for checking the permission of this attribute is AuthorizeCore which is marked as protected, so there is no way we can call this method from our code. So I decided to check, how other peoples are handling this issue, so far I have found two solutions:

  1. ASP.NET MVC Sitemap provider of Maarten Balliauw and
  2. MVCContrib.org.

But none of them are actually handling it correctly!!!.

Before moving to what is wrong with these solutions, let me paste the main portion of the AuthorizeAttribute code that is responsible for checking the permission:

public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
    public virtual void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (AuthorizeCore(filterContext.HttpContext))
        {
            HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
            cachePolicy.SetProxyMaxAge(new TimeSpan(0));
            cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
        }
        else
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
    }

    protected virtual bool AuthorizeCore(HttpContextBase httpContext)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException("httpContext");
        }

        IPrincipal user = httpContext.User;

        if (!user.Identity.IsAuthenticated)
        {
            return false;
        }

        if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
        {
            return false;
        }

        if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
        {
            return false;
        }

        return true;
    }
}

As you can see the only public method OnAuthorization (which is also an implementation of IAuthorizationFilter interface, we will discuss this interface after a little while) is calling the protected AuthorizeCore to check the permission, if it is permitted then it is adding some callback to sync with the ASP.NET Caching otherwise it returns Unauthorized Result.

Now lets see, what is wrong with the above two solutions, First the ASP.NET MVC SiteMap provider, the code that is responsible for checking the controller/action permission is the following:

IController controller = provider.GetController(requestContext, mvcNode.Controller); // get controller
if (controller is ControllerBase)
{
    var controllerContext = new ControllerContext(requestContext, (ControllerBase)controller);
    var authorizationContext = new AuthorizationContext(controllerContext);

    foreach (IAuthorizationFilter att in controller.GetType().GetCustomAttributes(typeof(IAuthorizationFilter), true).Union(// get controller authorization filters
    controller.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(m =>
    {
        var nameAtt = m.GetCustomAttributes(typeof(ActionNameAttribute), true).FirstOrDefault() as ActionNameAttribute;
        return string.Equals(mvcNode.Action, nameAtt != null ? nameAtt.Name : m.Name, StringComparison.OrdinalIgnoreCase);
    })
    .SelectMany(m => m.GetCustomAttributes(typeof(IAuthorizationFilter), true))))// get authorization filters from all related methods
    {
        if (att is ValidateAntiForgeryTokenAttribute)
        {
            return true;
        }
        att.OnAuthorization(authorizationContext); // run authorization
        if (authorizationContext.Result != null) // authorization failed
        {
            return false;
        }
    }
}

There are two issues with the above code:

  1. It depends upon the IAuthorizationFilter rather than AuthorizeAttribute, the problem with this approach is there more filters other than AuthorizeAttribute which implements this interface, as you can see there is a check for ValidateAntiForgeryTokenAttribute in the above code, there are few more already in the ASP.NET MVC Framework e.g. ValidateInputAttribute, RequireSslAttribute and anyone can write a custom filter that implements this interface and decorate the controller/action, so this code will return incorrect results, in those cases.
  2. The next issue, when it is executed for AuthorizeAttribute which in turns adds caching callback and there is no way to remove that callback once the OnAuthorization calls gets completed, so it will screw the ASP.NET Caching and might hurt the application performance.

The correct way to address this issue:

  1. Don’t depend on the IAuthorizationFilter interface, instead check the concrete AuthorizeAttribute which is meant to be dealing with Roles/Users, also it is not marked as sealed, so it is expected that you will be inheriting this class and override the AuthorizeCore method if you want to put your custom logic.
  2. Find a way to call the protected AuthorizeCore method which is actually responsible for checking the permission.

Now, lets see how the MVCContrib team is handling this issue, basically they have created a new class inherited from the AuthorizeAttribute and it also contains a public method Authorized which in turn calls the protected AuthorizeCore method:

public class OpenAuthorizeAttribute : AuthorizeAttribute
{
    public OpenAuthorizeAttribute(AuthorizeAttribute attribute)
    {
        Order = attribute.Order;
        Roles = attribute.Roles;
        Users = attribute.Users;
    }

    public bool Authorized(RequestContext requestContext)
    {
        return AuthorizeCore(requestContext.HttpContext);
    }
}

And whenever they wants to check permission they pass the actual attribute to this new class and calls the Authorized method. Much better than the previous solution, still it will not work when you create a Custom AuthorizationAttribute, as it will call the AuthorizeCore of the original AuthorizationAttribute rather than yours.

After discussing with the ASP.NET MVC Team and Levi Broderick explained me really well that AuthorizeAttribute was not really designed to be used in this kind of scenario.To solve it, yes we can use InvokeMethod of reflection, but invoking a protected method will not work in medium trust environment. So the only option that is left to monkey patch this issue is IL rewritting. The logic is, when checking the permission it will check whether a custom AuthorizeAttribute is used, if not it will follow the same as the MVCContrib is currently doing, if a custom Authorization attribute is used, it will runtime create a inherited class with a public method which we will call to check the permission, of course there is slight a performance overhead associated, but we can easily overcome it with proper caching. Now lets take a quick look how we are checking whether the user has permission of an action, assuming that the AuthorizeAttributes are collected from another service:

foreach (AuthorizeAttribute authorizationAttribute in authorizationAttributes)
{
    if (authorizationAttribute != null)
    {
        try
        {
            Type currentAuthorizationAttributeType = authorizationAttribute.GetType();

            IAuthorizeAttribute subclassedAttribute = (currentAuthorizationAttributeType == typeof(AuthorizeAttribute)) ?
                                                       new InternalAuthorize() : // No need to use Reflection.Emit when asp.net mvc built-in attribute is used
                                                       reflectedAuthorizeAttributeCache.GetAttribute(currentAuthorizationAttributeType);

            subclassedAttribute.Order = authorizationAttribute.Order;
            subclassedAttribute.Roles = authorizationAttribute.Roles;
            subclassedAttribute.Users = authorizationAttribute.Users;

            // Copy the remaining properties (if there is any)
            objectCopier.Copy(authorizationAttribute, subclassedAttribute, "Order", "Roles", "Users" /* Excluded properties */);

            allowed = subclassedAttribute.IsAuthorized(requestContext.HttpContext);
        }
        catch
        {
            // do not allow on exception
            allowed = false;
        }

        if (!allowed)
        {
            break;
        }
    }
}

As you can see when the default AuthorizeAttribute is used we are creating InternalAttribute, if not we are requesting a caching service to get the runtime version of this attribute, for each custom attribute we are using Reflection.Emit to create a subclass of that attribute and then we are caching it so that we can use the same attribute without recreating it with Reflection.Emit. Also to avoid naming collision, we created an interface IAuthorizeAttribute which we implements with IL. The IAuthorizeAttribute contains the same properties as the original AuthorizeAttribute:

public interface IAuthorizeAttribute
{
    int Order
    {
        get;
        set;
    }

    string Roles
    {
        get;
        set;
    }

    string Users
    {
        get;
        set;
    }

    bool IsAuthorized(HttpContextBase httpContext);
}

Now here is the magic code that generates the class at runtime:

// (c) Copyright Telerik Corp. 
// This source is subject to the Microsoft Public License. 
// See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL. 
// All other rights reserved.

namespace Telerik.Web.Mvc.Infrastructure.Implementation
{
    using System;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Web;

    public class AuthorizeAttributeBuilder : IAuthorizeAttributeBuilder
    {
        private static readonly Type authorizeAttributeType = typeof(IAuthorizeAttribute);
        private static readonly ModuleBuilder module = CreateModuleBuilder();

        public ConstructorInfo Build(Type parentType)
        {
            Guard.IsNotNull(parentType, "parentType");

            string typeName = "$" + parentType.FullName.Replace(".", string.Empty);

            TypeBuilder typeBuilder = module.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, parentType, new[] { authorizeAttributeType });
            typeBuilder.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);
            typeBuilder.AddInterfaceImplementation(authorizeAttributeType);

            WriteProperty(parentType, typeBuilder, "Order", typeof(int));
            WriteProperty(parentType, typeBuilder, "Roles", typeof(string));
            WriteProperty(parentType, typeBuilder, "Users", typeof(string));
            WriteIsAuthorized(parentType, typeBuilder);

            Type type = typeBuilder.CreateType();

            return type.GetConstructor(Type.EmptyTypes);
        }

        private static void WriteProperty(Type parentType, TypeBuilder builder, string name, Type type)
        {
            string getName = "get_" + name;
            string setName = "set_" + name;

            MethodInfo parentGetMethod = parentType.GetMethod(getName, BindingFlags.Public | BindingFlags.Instance);
            MethodBuilder implementedGetMethod = builder.DefineMethod(getName, MethodAttributes.Public | MethodAttributes.Virtual, type, Type.EmptyTypes);
            ILGenerator getIl = implementedGetMethod.GetILGenerator();
            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Call, parentGetMethod);
            getIl.Emit(OpCodes.Ret);

            MethodInfo interfaceGetMethod = authorizeAttributeType.GetMethod(getName, BindingFlags.Public | BindingFlags.Instance);
            builder.DefineMethodOverride(implementedGetMethod, interfaceGetMethod);

            MethodInfo parentSetMethod = parentType.GetMethod(setName, BindingFlags.Public | BindingFlags.Instance);
            MethodBuilder implementedSetMethod = builder.DefineMethod(setName, MethodAttributes.Public | MethodAttributes.Virtual, typeof(void), new[] { type });
            ILGenerator setIl = implementedSetMethod.GetILGenerator();
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Call, parentSetMethod);
            setIl.Emit(OpCodes.Ret);

            MethodInfo interfaceSetMethod = authorizeAttributeType.GetMethod(setName, BindingFlags.Public | BindingFlags.Instance);
            builder.DefineMethodOverride(implementedSetMethod, interfaceSetMethod);
        }

        private static void WriteIsAuthorized(Type parentType, TypeBuilder builder)
        {
            MethodInfo protectedMethod = parentType.GetMethod("AuthorizeCore", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod);
            MethodBuilder implementedMethod = builder.DefineMethod("IsAuthorized", MethodAttributes.Public | MethodAttributes.Virtual, typeof(bool), new[] { typeof(HttpContextBase) });
            ILGenerator il = implementedMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Call, protectedMethod);
            il.Emit(OpCodes.Ret);

            MethodInfo interfaceMethod = authorizeAttributeType.GetMethod("IsAuthorized", BindingFlags.Public | BindingFlags.Instance);
            builder.DefineMethodOverride(implementedMethod, interfaceMethod);
        }

        private static ModuleBuilder CreateModuleBuilder()
        {
            const string Name = "InheritedAuthorizeAttributes";

            AssemblyName assemblyName = new AssemblyName(Name + "Assembly")
                                            {
                                                Version = typeof(AuthorizeAttributeBuilder).Assembly.GetName().Version
                                            };

            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(Name + "Module");

            return moduleBuilder;
        }
    }
}

There is only one public method Build where you have to pass the custom AuthorizeAttribute type, the rest will be taken care by it. And if you want to use the above code, you are permitted to do so, as it is licensed under MS-PL.

The moral of this story is though ASP.NET MVC is one of the most extensible framework by MS, yet there are some places where it requires some re-work, but it does not mean you would say some harsh word like some negative minded people of our community, instead you should look for the solution and share it.

Shout it
Filed under: , , , , ,

Comments

# ASP.NET MVC and Authorization and Monkey Patching – Kazi Manzur … | Webmaster Tools

Pingback from  ASP.NET MVC and Authorization and Monkey Patching – Kazi Manzur … | Webmaster Tools

# Twitter Trackbacks for ASP.NET MVC and Authorization and Monkey Patching - Kazi Manzur Rashid's Blog [asp.net] on Topsy.com

Pingback from  Twitter Trackbacks for                 ASP.NET MVC and Authorization and Monkey Patching - Kazi Manzur Rashid's Blog         [asp.net]        on Topsy.com

# ASP.NET MVC and Authorization and Monkey Patching - Kazi Manzur Rashid

Sunday, September 06, 2009 9:08 AM by DotNetShoutout

Thank you for submitting this cool story - Trackback from DotNetShoutout

# re: ASP.NET MVC and Authorization and Monkey Patching

Sunday, September 06, 2009 9:43 AM by Vladan Strigo

Just one thing...when defining a menu, please allow it to be defined in 3 ways:

- file

- code (fluent interface maybe?)

- attributes on actions (as you are reading the authorization info, you can read maybe the menu info from the actions themselves?)

Is that possible? What are the plans?

Thanks!

Vladan

# ASP.NET MVC and Authorization and Monkey Patching - Kazi Manzur …

Pingback from  ASP.NET MVC and Authorization and Monkey Patching - Kazi Manzur …

# ASP.NET MVC Archived Blog Posts, Page 1

Monday, September 07, 2009 12:32 AM by ASP.NET MVC Archived Blog Posts, Page 1

Pingback from  ASP.NET MVC Archived Blog Posts, Page 1

# re: ASP.NET MVC and Authorization and Monkey Patching

Monday, September 07, 2009 2:54 AM by Maarten Balliauw

The MvcSiteMap project has been updated to reflect the correct way of doing things. Thanks for pointing this out!

# re: ASP.NET MVC and Authorization and Monkey Patching

Monday, September 07, 2009 6:39 AM by Kazi Manzur Rashid

@Vladan Strigo: Yes those are considered ;-), also there are few more innovative ideas that we are cooking.

@Maarten : Great.

# ASP.NET MVC 2 and why Dynamic Area is not supported

Wednesday, December 30, 2009 5:48 AM by Kazi Manzur Rashid's Blog

In my previous post , I mentioned that Area is one of the thing that the coming ASP.NET MVC2 needs heavy

# ASP.NET MVC 2 and why Dynamic Area is not supported

Wednesday, December 30, 2009 5:48 AM by Kazi Manzur Rashid's Blog

In my previous post , I mentioned that Area is one of the thing that the coming ASP.NET MVC2 needs heavy

# ASP.NET MVC 2 and why Dynamic Area is not supported

Wednesday, December 30, 2009 5:49 AM by Community Blogs

In my previous post , I mentioned that Area is one of the thing that the coming ASP.NET MVC2 needs heavy

# ASP.NET MVC 2 and why Dynamic Area is not supported

Wednesday, December 30, 2009 5:54 AM by Kazi Manzur Rashid's Blog

In my previous post , I mentioned that Area is one of the thing that the coming ASP.NET MVC2 needs heavy

# ASP.NET MVC 2 and why Dynamic Area is not supported | I love .NET!

Pingback from  ASP.NET MVC 2 and why Dynamic Area is not supported | I love .NET!