ASP.NET Core

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

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.

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

Disqus Comments Loading...
Share
Published by
Karthik Chintala

Recent Posts

2 Good Tools To Test gRPC Server Applications

In this post, we’ll see how to test gRPC Server applications using different clients. And… Read More

1 year ago

Exploring gRPC project in ASP.NET Core

In this post, we'll create a new gRPC project in ASP.NET Core and see what's… Read More

2 years ago

Run dotnet core projects without opening visual studio

In this blog post, we’ll see how to run dotnet core projects without opening visual… Read More

2 years ago

Programmatically evaluating policies in ASP.NET Core

Programmatically evaluating policies is useful when we want to provide access or hide some data… Read More

2 years ago

Policy-Based Authorization in ASP.NET Core

What is policy-based authorization and how to set up policy-based authorization with handlers and policies… Read More

2 years ago

Role-based Authorization in ASP.NET Core

What is role-based authorization? As the name says, role-based authorization authorizes a user based on… Read More

2 years ago

This website uses cookies.