How to check custom attributes inside authorization process (policy or middleware)?

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP



How to check custom attributes inside authorization process (policy or middleware)?



Main goal is to prevent access to the portal when OIDC user has custom claim with type 'BlockedFrom', which added in ClaimsTransformation.



I've solved it by middleware in Startup.Configure method. General reason is to keep original request URL without redirection to /Account/AccessDenied page.


Startup.Configure


app.Use((context, next) =>

var user = context.User;

if (user.IsAuthenticated())

// Do not rewrite path when it marked with custom [AllowBlockedAttribute]!
// /Home/Logout, for example. But how?
//
if (user.HasClaim(x => x.Type == UserClaimTypes.BlockedFrom))

// Rewrite to run specific method of HomeController for blocked users
// with detailed message.
//
context.Request.Path = GenericPaths.Blocked;



return next();
);



But have one unexpected result: the Logout method of HomeController is blocked too. User can't logout when blocked, hah!
The first thing that came to mind - check custom attribute such like [AllowBlockedAttribute]. Hardcoded path constants in middleware looks crazy. How to access attributes of calling method in middleware?


Logout


HomeController


[AllowBlockedAttribute]



Another (and more elegant) way is to put this logic to custom BlockedHandler : AuthorizationHandler<BlockedRequirement> and assign it in MVC options of Startup.ConfigureServices method as general policy:


BlockedHandler : AuthorizationHandler<BlockedRequirement>


Startup.ConfigureServices


services.AddSingleton<IAuthorizationHandler, BlockedHandler>();

services.AddMvc(options =>

var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new BlockedRequirement())
.Build();

// Set the default authentication policy to require users to be authenticated.
//
options.Filters.Add(new AuthorizeFilter(policy));

).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);



Hypothetical implementation of BlockedHandler:


BlockedHandler


public class BlockedHandler : AuthorizationHandler<BlockedRequirement>

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BlockedRequirement requirement)

if (!context.User.HasClaim(c => c.Type == UserClaimTypes.BlockedFrom))

context.Succeed(requirement);
return Task.CompletedTask;


// User is blocked!

if (context.Resource is AuthorizationFilterContext mvcContext)

if (mvcContext.ActionDescriptor is ControllerActionDescriptor descriptor)

var allowBlocked = descriptor.ControllerTypeInfo.CustomAttributes
.Concat<CustomAttributeData>(descriptor.MethodInfo.CustomAttributes)
.Any(x => x.AttributeType == typeof(AllowBlockedAttribute));

// User can access called action.
//
if (allowBlocked)
context.Succeed(requirement);


// Ugly to call this as the next step?
// mvcContext.HttpContext.Request.Path = GenericPaths.Blocked;


// Prevent redirection to AccessDenied
// Stop authorization chain.

return Task.CompletedTask;




Ok, now we can handle custom attribute. Seems that AuthorizationHandler is not a best place to tell HttpContext to change it's RequestPath without redirection. Where it can be done?




2 Answers
2



I've done some digging in the framework sources and found a way to make this work in the authorization handler way.



The entry point to the authorization process is AuthorizeFilter. The filter context has a Result property accepting an IActionResult. By setting this property you can short-circuit the request and display whatever action result (including a view) you want. This is the key to the solution.



If you follow the execution path, you realize that the filter context is passed to the authorization components and is available in the IAuthorizationHandler.HandleRequirementAsync method. You can get it from the Resource property of the context object by a downcast (as showed by OP already).



There's one more important thing: you must return success from the authorization handler, otherwise you end up with a redirect inevitably. (This becomes clear if you check out the default implementation of IPolicyEvaluator.)



So putting this all together:


public class BlockedHandler : AuthorizationHandler<BlockedRequirement>

private Task HandleBlockedAsync(AuthorizationFilterContext filterContext)

// create a model for the view if needed...
var model = new BlockedModel();

// do some processing if needed...

var modelMetadataProvider = filterContext.HttpContext.RequestServices.GetService<IModelMetadataProvider>();
// short-circuit request by setting the action result
filterContext.Result = new ViewResult

StatusCode = 403, // Client cannot access the requested resource

ViewName = "~/Views/Shared/Blocked.cshtml",
ViewData = new ViewDataDictionary(modelMetadataProvider, filterContext.ModelState) Model = model
;

return Task.CompletedTask;


protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, BlockedRequirement requirement)

if (context.User.HasClaim(c => c.Type == UserClaimTypes.BlockedFrom) &&
context.Resource is AuthorizationFilterContext filterContext &&
filterContext.ActionDescriptor is ControllerActionDescriptor descriptor)

var allowBlocked = descriptor.ControllerTypeInfo.CustomAttributes
.Concat(descriptor.MethodInfo.CustomAttributes)
.Any(x => x.AttributeType == typeof(AllowBlockedAttribute));

if (!allowBlocked)
await HandleBlockedAsync(filterContext);


// We must return success in every case to avoid forbid/challenge.
context.Succeed(requirement);






I have doubts about the building of the View in the authorization handler, but it really works such as desired. Good thing is that user can't access this page stub directly by calling the Controller Action and his status may be changed simply by page refresh.
– Arsync
Aug 12 at 21:17



I think the AuthorizationHandler is definitely the better place to put this logic. But - if I get it correctly - your problem is that the action to be invoked has already been selected by the time this handler executes so you cannot change the route any more.



Of course the standard way would be initiating a redirect but you want to avoid this to keep the current URL.



In light of the above, there's one way I can think of: a global action filter.



Action filters can run code immediately before and after an individual action method is called. They can be used to manipulate the arguments passed into an action and the result returned from the action.



The OnActionExecuting method seems the right place to put your logic to because at this point you have access to the attributes of the action method and you have the chance to short-circuit the processing (by setting the Result property of the ActionExecutingContext argument) if the user is blocked.



If you are not familiar with the concept of filters, you'll find all the details in this MSDN article.





Blocking as a part of the authorization process seems more accurate then action filter. It comes with AllowAnonymous bypass and other things as authorization filter in the pipeline. But I heard that overriding of the AuthorizeFilter is not a good practice. So it works as the requirement...
– Arsync
Aug 12 at 6:06





@Arsync Yeah, this code should be part of the authorization process, indeed. Check out my other answer.
– Adam Simon
Aug 12 at 11:25







By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

Firebase Auth - with Email and Password - Check user already registered

Dynamically update html content plain JS

How to determine optimal route across keyboard