Skip to content

Multiple authorization handlers for the same requirement in ASP.NET Core

multiple_authorizations_handlers_for_same_requirement on coderethinked.com

We saw how we could set up policy-based authorization in our previous article. In this article, we’ll focus on

  • Need for multiple handlers for the same requirement
  • How to setup multiple handlers for the same requirement (with demo)
  • What would happen if we call context.Fail in the authorization handler if the user does not have access
  • The execution order of multiple authorizations

We’ll use the same scenario as we used for our policy-based authorization: two lounges, one for premium users and one for standard users.

Let’s say we want to allow the trial/limited user to experience the standard and premium lounges (I know they are just images on our razor pages :P)

In this case, creating a new policy with both standard and premium access won’t help because we have to decorate the existing page model to have both the old policy and the new one. This adds an AND logic to the authorizations.

Let’s say we have added this to our code

[Authorize(Policy = "LimitedUser")]
[Authorize(Policy = "StandardOnly")]
public class StandardLoungeModel : PageModel
{
    public void OnGet()
    {
    }
}

The authorization handlers now will only allow access to the standard page model only if the user has both the LimitedUser and StandardOnly access.

Technically, here we need an OR logic to allow either limited users or standard users. This is where the multiple handlers for the same requirement come in.

Multiple handlers for the same requirements

To perform an OR-based evaluation, we will create multiple handlers for the same requirement so that we can bypass the user if the user has limited access. No need to create a new policy for the trial users.

Here is our original product access handler from our previous policy-based authorization post.

public class ProductAccessHandler : AuthorizationHandler<ProductAccessRequirement>
{
    private readonly IUserAccessRepository _userAccessRepository;
    public ProductAccessHandler(IUserAccessRepository userAccessRepository)
    {
        _userAccessRepository = userAccessRepository;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ProductAccessRequirement requirement)
    {
        var user = ClaimExtensions.GetClaim(context.User, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "nameidentifier");
        if (user == null) {
          context.Fail();
          return Task.CompletedTask;
        }

        var hasAccess = _userAccessRepository.HasAccess(user.Value, requirement.ProductIds, CancellationToken.None);
        if (hasAccess) context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

We will pass the user GUID and product Ids to the user access repository to see if the user has access to the resource. For more on how we set up the policy-based authorization please check this blog post.

Now, Let’s create a new TemporaryProductAccessHandler.

public class TemporaryProductAccessHandler : AuthorizationHandler<ProductAccessRequirement>
{
    private readonly IUserAccessRepository _userAccessRepository;

    public TemporaryProductAccessHandler(IUserAccessRepository userAccessRepository)
    {
        _userAccessRepository = userAccessRepository;
    }
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ProductAccessRequirement requirement)
    {
        var user = ClaimExtensions.GetClaim(context.User, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "nameidentifier");
        if (user == null) {
          context.Fail();
          return Task.CompletedTask;
        }

        var hasAccess = _userAccessRepository.HasAccess(user.Value, new string[]{ Constants.ProductCodes.LimitedUser }, CancellationToken.None);

        if (hasAccess)
            context.Succeed(requirement);
        
        return Task.CompletedTask;
    }
}

There’s a small difference between the product access handler and the temporary product access handler.

In our TemporaryProductAccessHandler, we will always pass in the LimitedUser as the product id to the HasAccess method instead of passing the product ids we get from the requirement so that the current user is evaluated against the limited user product access.

If the user has limited user as the product access, then we will invoke context.Succeed(requirement) to pass through the product access handler and the user should have access to the resource requested.

If an action method or a razor page model has decorated with Authorize(Policy = "PremiumOnly")] and the user just has limited access as product ID, the product access handler does not produce any success result but the TemporaryProductAccessHandler will check for limited user access and passes the authorization test.

This is how we can bypass the user having limited user access and provide access to all of the resources.

Registering our handler for the same requirement

Registering is the same as how we register any service with its appropriate interface.

builder.Services.AddSingleton<IAuthorizationHandler, ProductAccessHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, TemporaryProductAccessHandler>();

Once we registered our new TemporaryProductAccessHandler, we can simply run the app to see if the limited/temporary user is able to access the other pages. We don’t need to create new policies or add more Authorize attributes to the existing controllers or actions or razor page models.

Here is the demo of Limited user accessing both lounges

Limited users can access resources after multiple authorizations

If we invoked context.Fail() when a user does not have a product ID, what would happen?

Notice in our authorization handlers, we never called context.Fail() method though the user does not have access. We could do something like this in our ProductAccessHandler

var hasAccess = _userAccessRepository.HasAccess(user.Value, requirement.ProductIds, CancellationToken.None);

if (hasAccess)
    context.Succeed(requirement);
else 
    context.Fail();

f we implement an ProductAccessHandler like this, we can’t bypass the temporary user access restriction because we’re configuring the authorization to deny access if the user fails the product access test.

When any of the authorization handlers fail, access will be denied. But for authorization to succeed, at least one authorization handler must evaluate success.

We had the following code in our authorization handlers.

if (user == null) {
    context.Fail();
    return Task.CompletedTask;
}

If we encountered a null value for the user object we know the user is not logged in yet so we fail the authorization test.

But, in multiple authorizations, it’s better to ignore invoking context.Fail() when the user did not pass the authorization test because there could be another authorization handler that may check for attributes that could pass the authorization test.

Execution order of multiple authorization handlers

Although, Microsoft documentation claims that the handlers can execute in any order.

Execution order of authorization handlers

After debugging, I see the execution order of these handlers follows their order of registration in the Program.cs file.

So, if we registered TemporaryProductAccessHandler first and followed by ProductAccessHandler, the temporary handler gets fired first and product access gets the next hit.

builder.Services.AddSingleton<IAuthorizationHandler, ProductAccessHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, TemporaryProductAccessHandler>();

Here is the debugging demo of the execution order.

Anyway, we are not worried about the order of execution in our article.

References

1 thought on “Multiple authorization handlers for the same requirement in ASP.NET Core”

  1. Pingback: Dew Drop – March 27, 2023 (#3908) – Morning Dew by Alvin Ashcraft

Leave a Reply

Your email address will not be published.