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:
AuthorizeCore()
in
this case?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):
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.
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()
).ActionResult
, only the AuthorizeCore()
would be called and not the OnAuthorization()
.AuthorizationContext
anyway in this case.Therefore, sharing the AuthorizationContext
between the OnAuthorization()
and AuthorizeCore()
is not the option!
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With