Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC 5: Custom AuthorizeAttribute and Caching

I'm trying to find a solution for implementing custom System.Web.Mvc.AuthorizeAttribute by deriving from it and overriding some of its methods.
Every approach I'm trying, I'm facing with certain issues in the default authorization mechanism of the MVC 5 that prevents me from proper extending that.
I've done the huge research on this field on SO and many dedicated resources, but I couldn't get a solid solution for the scenario like my current one.

First limitation:
My authorization logic needs additional data like controller and method names and attributes applied to them rather than limited portion of the data HttpContextBase is able to provide.
Example:

public override void OnAuthorization(AuthorizationContext filterContext)
{
    ...
    var actionDescriptor = filterContext.ActionDescriptor;
    var currentAction = actionDescriptor.ActionName;
    var currentController = actionDescriptor.ControllerDescriptor.ControllerName;

    var hasHttpPostAttribute = actionDescriptor.GetCustomAttributes(typeof(HttpPostAttribute), true).Any();
    var hasHttpGetAttribute = actionDescriptor.GetCustomAttributes(typeof(HttpGetAttribute), true).Any();

    var isAuthorized = securitySettingsProvider.IsAuthorized(
        currenPrincipal, currentAction, currentController, hasHttpPostAttribute, hasHttpGetAttribute);
    ...
}

This is why I can't implement my authorization logic inside the AuthorizeCore() method override since it gets only HttpContextBase as the parameter and what I need to make an authorization decision is AuthorizationContext.
This leads me to put my authorization logic to the OnAuthorization() method override as in the example above.

But here we come to the second limitation:
The AuthorizeCore() method is called by the caching system to make an authorization decision whether the current request should be served with the cached ActionResult or corresponding controller method should be used to create a new ActionResult.
So we can't just forget about the AuthorizeCore() and use the OnAuthorization() only.

And here we're returning to the initial point:
How to make authorization decision for the cache system based on the HttpContextBase only if we need more data from the AuthorizationContext?
with many subsequent questions like:

  • How are we supposed to properly implement the AuthorizeCore() in this case?
  • Should I implement my own caching to let it supply sufficient data to the authorization system? And how it can be done if yes?
  • Or I should say good-bye to the caching for all controller methods protected with my custom System.Web.Mvc.AuthorizeAttribute? It must be said here that I'm going to use my custom System.Web.Mvc.AuthorizeAttribute as a global filter and this is the complete good-bye to the caching if the answer to this question is yes.

So the main question here:
What is the possible approaches around to deal with such custom authorization and proper caching?

UPDATE 1 (Additional information to address some possible answers around):

  1. There is no gurantee in the MVC that every single instance of the AuthorizeAttribute would serve single request. It can be reused for many requests (see here for more info):

    Action filter attributes must be immutable, since they may be cached by parts of the pipeline and reused. Depending on where this attribute is declared in your application, this opens a timing attack, which a malicious site visitor could then exploit to grant himself access to any action he wishes.

    In the other words, AuthorizeAttribute MUST be immutable and MUST NOT share state between any method calls.
    Moreover in the AuthorizeAttribute-as-global-filter scenario, a single instance of the AuthorizeAttribute is used to serve all request.
    If you think that you save AuthorizationContext in the OnAuthorization() for a request, you're then able to get it in subsequent AuthorizeCore() for the same request, you're wrong.
    As a result you would take authorization decision for the current request based on the AuthorizationContext from the other request.

  2. If a AuthorizeCore() is triggered by the caching layer, OnAuthorization() has never called before for the current request (please refer the sources of the AuthorizeAttribute starting from CacheValidateHandler() down to AuthorizeCore()).
    In the other words, if request is going to be served using the cached ActionResult, only the AuthorizeCore() would be called and not the OnAuthorization().
    So you're unable to save AuthorizationContext anyway in this case.

Therefore, sharing the AuthorizationContext between the OnAuthorization() and AuthorizeCore() is not the option!

like image 975
Alexander Abakumov Avatar asked Oct 28 '14 10:10

Alexander Abakumov


1 Answers

the OnAuthorization method is called before the AuthorizeCore method. So you can save the current context for later processing:

public class MyAttribute: AuthorizeAttribute
{
    # Warning - this code doesn't work - see comments

    private AuthorizationContext _currentContext;

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
         _currentContext = filterContext;
         base.OnAuthorization(filterContext);
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
         // use _currentContext
    }    
}

Edit

Since this will not work as Alexander pointed out. The second option could be to completely override the OnAuthorization method:

        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
            {
                throw new InvalidOperationException(MvcResources.AuthorizeAttribute_CannotUseWithinChildActionCache);
            }

            bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true)
                                     || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true);

            if (skipAuthorization)
            {
                return;
            }

            if (AuthorizeCore(filterContext.HttpContext))
            {
                HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
                cachePolicy.SetProxyMaxAge(new TimeSpan(0));

                var actionDescriptor = filterContext.ActionDescriptor;
                var currentAction = actionDescriptor.ActionName;
                var currentController = actionDescriptor.ControllerDescriptor.ControllerName;

                var hasHttpPostAttribute = actionDescriptor.GetCustomAttributes(typeof(HttpPostAttribute), true).Any();
                var hasHttpGetAttribute = actionDescriptor.GetCustomAttributes(typeof(HttpGetAttribute), true).Any();
                // fill the data parameter which is null by default
                cachePolicy.AddValidationCallback(CacheValidateHandler, new { actionDescriptor : actionDescriptor, currentAction: currentAction, currentController: currentController, hasHttpPostAttribute : hasHttpPostAttribute, hasHttpGetAttribute: hasHttpGetAttribute  });
            }
            else
            {
                HandleUnauthorizedRequest(filterContext);
            }
        }

    private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException("httpContext");
        }
        // the data will contain AuthorizationContext attributes
        bool isAuthorized = myAuthorizationLogic(httpContext, data);
        return (isAuthorized) ? HttpValidationStatus.Valid : httpValidationStatus.IgnoreThisRequest;

    }
like image 187
Marian Ban Avatar answered Sep 24 '22 04:09

Marian Ban