Why can't I combine [Authorize] and [OutputCache] attributes when using Azure cache (.NET MVC3 app)? - asp.net-mvc-3

Using Windows Azure's Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider as the outputCache provider for an MVC3 app. Here is the relevant action method:
[ActionName("sample-cached-page")]
[OutputCache(Duration = 300, VaryByCustom = "User",
Location = OutputCacheLocation.Server)]
[Authorize(Users = "me#mydomain.tld,another#otherdomain.tld")]
public virtual ActionResult SampleCachedPage()
{
return View();
}
I get the following exception when loading this view from a web browser:
System.Configuration.Provider.ProviderException: When using a custom output cache provider like 'DistributedCache', only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks.
System.Configuration.Provider.ProviderException: When using a custom output cache provider like 'DistributedCache', only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks.
at System.Web.Caching.OutputCache.InsertResponse(String cachedVaryKey, CachedVary cachedVary, String rawResponseKey, CachedRawResponse rawResponse, CacheDependency dependencies, DateTime absExp, TimeSpan slidingExp)
at System.Web.Caching.OutputCacheModule.OnLeave(Object source, EventArgs eventArgs)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
If I remove the [Authorize] attribute, the view caches as would be expected. Does this mean I cannot put [OutputCache] on an action method that must have [Authorize]? Or, do I need to override AuthorizeAttribute with a custom implementation that uses a static validation callback method for the cache?
Update 1
After Evan's answer, I tested the above action method in IIS Express (outside of Azure). Here is my override for the VaryByCustom = "User" property on the OutputCache attribute:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
return "User".Equals(custom, StringComparison.OrdinalIgnoreCase)
? Thread.CurrentPrincipal.Identity.Name
: base.GetVaryByCustomString(context, custom);
}
When I visit the sample cached page as me#mydomain.tld, the output of the page is cached, and the view displays "This page was cached at 12/31/2011 11:06:12 AM (UTC)". If I then sign out and sign in as another#otherdomain.tld and visit the page, it displays "This page was cached at 12/31/2011 11:06:38 AM (UTC)". Signing back in as me#mydomain.tld and revisiting the page causes the cache to display "This page was cached at 12/31/2011 11:06:12 AM (UTC)" again. Further sign in/out attempts show that different output is being cached & returned depending on the user.
This is leading me to believe that the output is being cached separately based on the user, which is the intention with my VaryByCustom = "User" setting & override. The problem is that it doesn't work with Azure's distributed cache provider. Evan, does you answer about only caching public content still stand?
Update 2
I dug up the source, and found that the out-of-box AuthorizeAttribute does in fact have a non-static validation callback. Here is an excerpt from OnAuthorization:
if (AuthorizeCore(filterContext.HttpContext)) {
// ** IMPORTANT **
// Since we're performing authorization at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether a page should be served from the cache.
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
else {
HandleUnauthorizedRequest(filterContext);
}
CacheValidationHandler delegates the cache validation to protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase), which of course is not static. One reason why it is not static is because, as noted in the IMPORTANT comment above, it invokes protected virtual bool AuthorizeCore(HttpContextBase).
In order to do any of the AuthorizeCore logic from a static cache validation callback method, it would need to know the Users and Roles properties of the AuthorizeAttribute instance. However there doesn't seem to be an easy way to plug in. I would have to override OnAuthorization to put these 2 values into the HttpContext (Items collection?) and then override OnCacheAuthorization to get them back out. But that smells dirty.
If we are careful to use the VaryByCustom = "User" property in the OutputCache attribute, can we just override OnCacheAuthorization to always return HttpValidationStatus.Valid? When the action method does not have an OutputCache attribute, we would not need to worry about this callback ever being invoked, correct? And if we do have an OutputCache attribute without VaryByCustom = "User", then it should be obvious that the page could return any cached version regardless of which user request created the cached copy. How risky is this?

Caching happens before the Action. You will likely need to customize your authorization mechanics to handle cache scenarios.
Check out a question I posted a while back - MVC Custom Authentication, Authorization, and Roles Implementation.
The part I think would help you is a custom Authorize Attribute who's OnAuthorize() method deals with caching.
Below is a code block for example:
/// <summary>
/// Uses injected authorization service to determine if the session user
/// has necessary role privileges.
/// </summary>
/// <remarks>As authorization code runs at the action level, after the
/// caching module, our authorization code is hooked into the caching
/// mechanics, to ensure unauthorized users are not served up a
/// prior-authorized page.
/// Note: Special thanks to TheCloudlessSky on StackOverflow.
/// </remarks>
public void OnAuthorization(AuthorizationContext filterContext)
{
// User must be authenticated and Session not be null
if (!filterContext.HttpContext.User.Identity.IsAuthenticated || filterContext.HttpContext.Session == null)
HandleUnauthorizedRequest(filterContext);
else {
// if authorized, handle cache validation
if (_authorizationService.IsAuthorized((UserSessionInfoViewModel)filterContext.HttpContext.Session["user"], _authorizedRoles)) {
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object o, ref HttpValidationStatus status) => AuthorizeCache(context), null);
}
else
HandleUnauthorizedRequest(filterContext);
}
}
/// <summary>
/// Ensures that authorization is checked on cached pages.
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public HttpValidationStatus AuthorizeCache(HttpContext httpContext)
{
if (httpContext.Session == null)
return HttpValidationStatus.Invalid;
return _authorizationService.IsAuthorized((UserSessionInfoViewModel) httpContext.Session["user"], _authorizedRoles)
? HttpValidationStatus.Valid
: HttpValidationStatus.IgnoreThisRequest;
}

I've come back to this issue and, after a bit of tinkering, have concluded that you cannot use the out of the box System.Web.Mvc.AuthorizeAttribute along with the out of the box System.Web.Mvc.OutputCacheAttribute when using the Azure DistributedCache. The main reason is because, as the error message in the original question states, the validation callback method must be static in order to use it with Azure's DistributedCache. The cache callback method in the MVC Authorize attribute is an instance method.
I went about trying to figure out how to make it work by making a copy of the AuthorizeAttribute from the MVC source, renaming it, hooking it up to an action with OutputCache connected to Azure, and debugging. The reason the cache callback method is not static is because, in order to authorize, the attribute needs to check the HttpContext's User against the Users and Roles property values that are set when the attribute is constructed. Here is the relevant code:
OnAuthorization
public virtual void OnAuthorization(AuthorizationContext filterContext) {
//... code to check argument and child action cache
if (AuthorizeCore(filterContext.HttpContext)) {
// Since we're performing authorization at the action level,
// the authorization code runs after the output caching module.
// In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would
// later be served the cached page. We work around this by telling
// proxies not to cache the sensitive page, then we hook our custom
// authorization code into the caching mechanism so that we have
// the final say on whether a page should be served from the cache.
HttpCachePolicyBase cachePolicy = filterContext
.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
else {
HandleUnauthorizedRequest(filterContext);
}
}
Cache Validation Callback
private void CacheValidateHandler(HttpContext context, object data,
ref HttpValidationStatus validationStatus) {
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
// This method must be thread-safe since it is called by the caching module.
protected virtual HttpValidationStatus OnCacheAuthorization
(HttpContextBase httpContext) {
if (httpContext == null) {
throw new ArgumentNullException("httpContext");
}
bool isAuthorized = AuthorizeCore(httpContext);
return (isAuthorized)
? HttpValidationStatus.Valid
: HttpValidationStatus.IgnoreThisRequest;
}
As you can see, the cache validation callback ultimately invokes AuthorizeCore, which is another instance method (protected virtual). AuthorizeCore, which was also called during OnAuthorization, does 3 main things:
Checks that the HttpContextBase.User.Identity.IsAuthenticated == true
If the attribute has a non-empty Users string property, checks that the HttpContextBase.User.Identity.Name matches one of the comma-separated values.
If the attribute has a non-empty Roles string property, checks that the HttpContextBase.User.IsInRole for one of the comma-separated values.
AuthorizeCore
// This method must be thread-safe since it is called by the thread-safe
// OnCacheAuthorization() method.
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;
}
When you simply try to make the validation callback method static, the code won't compile because it needs access to these _rolesSplit and _usersSplit fields, which are based on the public Users and Roles properties.
My first attempt was to pass these values to the callback using the object data argument of the CacheValidateHandler. Even after introducing static methods, this still did not work, and resulted in the same exception. I was hoping that the object data would be serialized, then passed back to the validate handler during the callback. Apparently this is not the case, and when you try to do this, Azure's DistributedCache still considers it a non-static callback, resulting in the same exception & message.
// this won't work
cachePolicy.AddValidationCallback(CacheValidateHandler, new object() /* data */);
My second attempt was to add the values to the HttpContext.Items collection, since an instance of HttpContext is automatically passed to the handler. This didn't work either. The HttpContext that is passed to the CacheValidateHandler is not the same instance that existed on the filterContext.HttpContext property. In fact, when the CacheValidateHandler executes, it has a null Session and always has an empty Items collection.
// this won't work
private void CacheValidateHandler(HttpContext context, object data,
ref HttpValidationStatus validationStatus) {
Debug.Assert(!context.Items.Any()); // even after I put items into it
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
However...
Even though there seems to be no way to pass the Users & Roles property values back to the cache validation callback handler, the HttpContext passed to it does in fact have the correct User Principal. Also, none of the actions where I currently want to combine [Authorize] and [OutputCache] ever pass a Users or Roles property to the AuthorizeAttribute constructor.
So, it is possible to create a custom AuthenticateAttribute which ignores these properties, and only checks to make sure the User.Identity.IsAuthenticated == true. If you need to authenticate against a specific role, you could also do so and combine with OutputCache... however, you would need a distinct attribute for each (set of) Role(s) in order to make the cache validation callback method static. I will come back and post the code after I've polished it a bit.

You are correct olive. Caching works by caching the entire output of the Action (including all attributes) then returning the result to subsequent calls without actually calling any of your code.
Because of this you cannot cache and check authorization because by caching you are not going to call any of your code (including authorization). Therefore anything that is cached must be public.

Related

How can I extend the Authorize attribute's logic in ASP.NET Core 3.1?

I'm working on an ASP.NET Core MVC application and I want to be able to better handle what happens when an unauthenticated user runs an action through AJAX.
I found this solution that essentially extends the Authorize attribute's logic and sets an AJAX request's status code to 401 when user is no longer authenticated. The status code is returned to a global AJAX error handler and the appropriate action can be performed.
I'm trying to create the attribute in Core 3.1 but I cannot find a way to first run the base logic of the filter. base.OnAuthorization method is not there. I specifically don't want to rewrite the Authorize attribute's functionality - only to extend it.
How can I extend the Authorize attribute's logic in Core 3.1?
The authorize attribute I'm writing:
public class AuthorizeUserAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
base.OnAuthorization(context); //method does not exist
OnAjaxAuthorization(context);
}
internal void OnAjaxAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.User.Identity.IsAuthenticated == false)
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
}
}
}
Just as a side note, I was thinking of writing a ActionFilterAttribute to run the additional code instead, but I can't do that because the Authorize attribute always runs first.

ASP.NET Core 2.2 - Action Filter db Query Question

I have users in our app, who are mapped to companies. When a user logs in and starts to make requests I want a way to validate if that user is currently mapped to the company for access to company resources.
The idea I had was to create a whole controller just to manage all of this, but someone mentioned ActionFilters as a much better and cleaner option, I have to agree after looking at it.
The idea is to have the controller setup as:
controller - action - CompanyId - ReportId
So any request to root the system would just look up if there are any companies mapped to that logged in user.
But if the request included CompanyId then they'd go to that company's “portal” account page. It's really any request that includes CompanyId where I want the actionFilter to make a determination on if that user is allowed access.
Request comes in...
There is a CompanyId in the request!
ActionFilter:
Look up in db for all users assigned to that CompanyId. Is current user within that list? No? = kick'em out.
I tried to type in a code example, but the system told me to manually indent each line by 4 spaces, I was doing it from memory anyways so no idea how helpful it would have been anyways.
You could get your action parameters in your action filter and then get your database via HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>().Refer to here.
public class TestActionFilter:Attribute,IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
//If companyId is action parameter
var companyId= context.ActionArguments["companyId"].ToString();
//If companyId1 is query string
var companyId1= context.HttpContext.Request.Query["companyId1"].ToString();
//If companyId2 is in request header
var companyId2= context.HttpContext.Request.Headers["companyId2"].ToString();
//get your dbcontext
var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
//EF core logic
//...
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
You could use it on action directly using [TestActionFilter] attribute or set as global filter
services.AddMvc(options =>
{
options.Filters.Add(new TestActionFilter()); // an instance
});

Global redirect based on logic in ASP.NET MVC3

I am building an ASP.NET MVC3 computer support ticketing portal.
There is a maintenance state, where it is best to forbid the users from interacting with EF/Database, to avoid "collisions" I am currently getting.
I have an IMaintenanceDispatcher that has a boolean property IsOnMaintenance set to true by the business logic, whenever a background logic puts the portal in that state.
I need to redirect client requests to a parking page for the time of maintenance.
Where do I place the logic that will check if the IsOnMaintenance is true, and if so, do a redirect to a URL?
You could put it in an ActionFilterAttribute and apply that attribute to any applicable actions/controllers or globally.
public class IsOnMaintenanceAttribute : ActionFilterAttribute
{
//You'll need to setup your IoC to inject this
public IMaintenanceDispatcher InjectedMaintenanceDispatcher { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
object ticketIdObj;
if (!filterContext.ActionParameters.TryGetValue("ticketId", out ticketIdObj))
return;
//Make sure it exists
if (InjectedMaintenanceDispatcher.IsOnMaintenance(int.parse(ticketIdObj)))
{
var routeValues = new RouteValueDictionary(new {
action = "parkingpage",
controller = "maintenance",
area = "ticket" });
filterContext.Result = new RedirectToRouteResult(routeValues);
return;
}
}
}
Note, your action method parameters needs to contain a variable named ticketId for the filterContext.ActionParameters.TryGetValue to work.
Note: I had assumed that an individual ticket is put into maintenance mode and you were wanting to check for that... but re-reading the question it seems like you want to put the whole area/site on hold. Even with that case, the ActionFilterAttribute example still holds... just not as different.

Can I validate HTTP request signature tokens and nonces using Model Binding?

I am setting up an end-point using ASP.NET MVC to which requests can be made to manipulate and retrieve data (basically, an API). I am using a 2-legged OAuth model to validate that requests be signed using a secret key and signing method as well as a nonce table to prevent hi-jacking.
Since Model Binding is so handy in ASP.NET MVC I am going to take advantage of it to consume requests, but I wonder if I can bake the signature verification and nonce/timestamp handling right into the model binder. Is this possible? That way I can just re-use the implementation on the various Actions that I create.
I reckon you should be able to. Try this:
public class FooModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
FooModel fooModel = bindingContext.Model as fooModel;
if (fooModel != null)
{
// Do your verification stuff in here
// Updating any properties of your Model.
// Or you could retrieve something else entirely and return it if you like
// Let's pretend we just want to verify the model and set some property or other.
fooModel.NonceOkay = DoVerification(fooModel);
fooModel.NextAction = WorkOutWhereToGoNext(fooModel);
// or whatever
}
return fooModel;
}
}
DoVerification could live in your ModelBinder, but it might be better for it to live somewhere else.
Then stick this in Application_Start in your Global.asax:
ModelBinders.Binders.Add(typeof(Foo), new FooModelBinder());

mvc3 OutputCache RemoveOutputCacheItem RenderAction

I did my research but haven't found any answers.
I'm using Html.RenderAction in a masterpage ( to render page header with links specific to user permissions ). Action is decorated with OutputCache, returns partial control and gets cached as expected.
When the event happens ( let's say permissions are changed ) I want to programmatically invalidate cached partial control.
I'm trying to use RemoveOutputCacheItem method. It takes a path as a parameter. I'm trying to set the path to the action used in Html.RenderAction. That doesn't invalidate the action.
How can I programmatically invalidate the action?
Thanks
The cache for child actions is stored in the OutputCacheAttribute.ChildActionCache property. The problem is that the API generating ids for child actions and storing them in this object is not public (WHY Microsoft??). So if you try to loop through the objects in this collection you will discover that it will also contain the cached value for your child action but you won't be able to identify it unless you reverse engineer the algorithm being used to generate keys which looks something like this (as seen with Reflector):
internal string GetChildActionUniqueId(ActionExecutingContext filterContext)
{
StringBuilder builder = new StringBuilder();
builder.Append("_MvcChildActionCache_");
builder.Append(filterContext.ActionDescriptor.UniqueId);
builder.Append(DescriptorUtil.CreateUniqueId(new object[] { this.VaryByCustom }));
if (!string.IsNullOrEmpty(this.VaryByCustom))
{
string varyByCustomString = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, this.VaryByCustom);
builder.Append(varyByCustomString);
}
builder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(this.VaryByParam)));
using (SHA256 sha = SHA256.Create())
{
return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())));
}
}
So you could perform the following madness:
public ActionResult Invalidate()
{
OutputCacheAttribute.ChildActionCache = new MemoryCache("NewDefault");
return View();
}
which obviously will invalidate all cached child actions which might not be what you are looking for but I am afraid is the only way other than of course reverse engineering the key generation :-).
#Microsoft, please, I am begging you for ASP.NET MVC 4.0:
introduce the possibility to do donut caching in addition to donut hole caching
introduce the possibility to easily expire the result of a cached controller action (something more MVCish than Response.RemoveOutputCacheItem)
introduce the possibility to easily expire the result of a cached child action
if you do 1. then obviously introduce the possibility to expire the cached donut portion.
You might want to approach this a different way. You could create a custom AuthorizeAttribute -- it would simply allow everyone -- and add override the OnCacheValidation method to incorporate your logic. If the base OnCacheValidation returns HttpValidationStatus.Valid, then make your check to see if the state has changed and if so, return HttpValidationStatus.Invalid instead.
public class PermissionsChangeValidationAttribute : AuthorizeAttribute
{
public override OnAuthorization( AuthorizationContext filterContext )
{
base.OnAuthorization( filterContext );
}
public override HttpValidationStatus OnCacheAuthorization( HttpContextBase httpContext )
{
var status = base.OnCacheAuthorization( httpContext );
if (status == HttpValidationStatus.Valid)
{
... check if the permissions have changed somehow
if (changed)
{
status = HttpValidationStatus.Invalid;
}
}
return status;
}
}
Note that there are ways to pass additional data in the cache validation process if you need to track the previous state, but you'd have to replicate some code in the base class and add your own custom cache validation handler. You can mine some ideas on how to do this from my blog post on creating a custom authorize attribute: http://farm-fresh-code.blogspot.com/2011/03/revisiting-custom-authorization-in.html

Resources