Alexander Abakumov Alexander Abakumov - 3 months ago 50
C# Question

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!

Answer

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

public class MyAttribute: AuthorizeAttribute
{
    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;

    }
Comments