blog post image
Andrew Lock avatar

Andrew Lock

~8 min read

Custom authorisation policies and requirements in ASP.NET Core

This post is the next in a series of posts on the authentication and authorisation infrastructure in ASP.NET Core . In the previous post we showed the basic framework for authorisation in ASP.NET Core i.e. restricting access to parts of your application depending on the current authenticated user. We introduced the concept of Policies, to decouple your authorisation logic from the underlying roles and claims of users. Finally, we showed how to create simple policies that verify the existence of a single claim or role.

In this post we look at creating more complex policies with multiple requirements, creating a custom requirement, and applying an authorisation policy to your entire application.

Policies with multiple requirements

In the previous post, I showed how we could create a simple policy, named CanAccessVIPArea to verify whether a user is allowed to access VIP related methods. This policy tested for a single claim on the User, and authorised the user if the policy was satisfied. For completeness, this is how we configured it in our Startup class:

public void ConfigureServices(IServiceCollection services)  
{
    services.AddMvc();

    services.AddAuthorization(options =>
    {
        options.AddPolicy(
            "CanAccessVIPArea",
            policyBuilder => policyBuilder.RequireClaim("VIPNumber"));
    });
}

Imagine now that the original requirements have changed. For example, consider this policy as being applied to the VIP lounge at an airport. In the current implementation, you would be allowed to enter, only if you have a VIP number. However, we now want to ensure that employees of the airline are also allowed to use the VIP lounge, as well as the CEO of the airport.

When you first consider the problem, you might see the policyBuilder object above, notice that it provides a fluent interface, and be tempted to chain additional RequireClaim() calls to it, something like

policyBuilder => policyBuilder
    .RequireClaim("VIPNumber")
    .RequireClaim("EmployeeNumber")
    .RequireRole("CEO"));

Unfortunately this won't produce the desired behaviour. Each of the requirements that make up the policy must be satisfied, i.e. they are combined using AND whereas we have an OR requirement. To pass the policy in this current state, you would need to have a VIPNumber, an EmployeeNumber and also be a CEO!

Creating a custom policy using a Func

There are a number of different approaches available to satisfy our business requirement, but as the policy is simple to express in this case, we will simply use a Func<AuthorizationHandlerContext, bool> provided to the PolicyBuilder.RequireAssertion method:

services.AddAuthorization(options =>
{
    options.AddPolicy(
        "CanAccessVIPArea",
        policyBuilder => policyBuilder.RequireAssertion(
            context => context.User.HasClaim(claim => 
                           claim.Type == "VIPNumber" 
                           || claim.Type == "EmployeeNumber")
                        || context.User.IsInRole("CEO"))
        );
});

To satisfy this requirement we are returning a simple bool to indicate whether a user is authorised based on the policy. We are provided an AuthorizationHandlerContext which provides us access to the current ClaimsPrincipal via the User property. This allows us to verify the claims and role of the user.

As you can see from our logic, our "CanAccessVIPArea" policy will now authorise if any of our original business requirements are met, which provides multiple ways to authorise a user.

Creating a custom requirement

While the above approach works for the simplest requirements, it's easy to see that as the rules become more complicated, your policy code could quickly become unmanageable. Additionally, you may need access to other services via dependency injection. In these cases, it's worth considering creating custom requirements and handlers.

Before I jump into the code, a quick recap on the terminology used here:

  • We have a Resource that needs to be protected (e.g. an MVC Action) so that only some users may be authorised to access it,
  • A resource may be protected by one or more Policies (e.g. CanAccessVIPArea). All policies must be satisfied in order for access to the resource to be granted.
  • Each Policy has one or more Requirements (e.g. IsVIP, IsBookedOnToFlight). All requirements must be satisfied on a policy for the overall policy to be satisfied.
  • Each Requirement has one or more Handlers. A requirement is satisfied, if any of them return a Success result, and none of them return an explicit Fail result.

With this in mind, we will redesign our VIP policy above to use a custom requirement, and create some handlers for it.

The Requirement

A requirement in ASP.NET Core is a simple class that implements the empty marker interface IAuthorizationRequirement. You can also use it to store any additional parameters for use later. We have extended our basic VIP requirement described previously to also provide an Airline, so that we only allow employees of the given airline to access the VIP lounge:

public class IsVipRequirement : IAuthorizationRequirement
{
    public IsVipRequirement(string airline)
    {
        Airline = airline;
    }

    public string Airline { get; }
}

The Authorisation Handlers

The authorisation handler is where all the work of authorising a requirement takes place. To implement a handler you inherit from AuthorizationHandler<T>, and implement the HandleRequirementAsync() method. As mentioned previously, a requirement can have multiple handlers, and only one of these needs to succeed for the requirement to be satisfied.

In our business requirement, we have three handlers corresponding to the three different ways to satisfy the requirement. Each of these are presented and explained below. I will also add an additional handler which checks whether a user has been banned from the VIP lounge previously, so shouldn't be let in again!

The simplest handler is the 'CEO' handler. This simply checks if the current authenticated user is in the role "CEO". If they are, then the handler calls Succeed on the underlying requirement. A default task is returned at the end of the method as the method is asynchronous. Note that in the case that the requirement is not fulfilled, we do nothing with the context; if cannot fulfill it with the current handler, we leave it for the next handler to deal with.

public class IsCEOAuthorizationHandler : AuthorizationHandler<IsVipRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsVipRequirement requirement)
    {
        if (context.User.IsInRole("CEO"))
        {
            context.Succeed(requirement);
        }
        return Task.FromResult(0);
    }
}

The VIP number handler is much the same, it performs a simple check that the current ClaimsPrincipalcontains a claim of type "VIPNumber", and if so, satisfies the requirements.

public class HasVIPNumberAuthorizationHandler : AuthorizationHandler<IsVipRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsVipRequirement requirement)
    {
        if (context.User.HasClaim(claim => claim.Type == "VIPNumber"))
        {
            context.Succeed(requirement);
        }
        return Task.FromResult(0);
    }
}

Our next handler is the 'employee' handler. This verifies that the authenticated user has a claim of type 'EmployeeNumber', and also that this claim was issued by the given Airline. We will see shortly where the requirement object passed in comes from, but you can see that we can access its Airline property and use that within our handler:

public class IsAirlineEmployeeAuthorizationHandler : AuthorizationHandler<IsVipRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsVipRequirement requirement)
    {
        if (context.User.HasClaim(claim =>
            claim.Type == "EmployeeNumber" && claim.Issuer == requirement.Airline))
        {
            context.Succeed(requirement);
        }
        return Task.FromResult(0);
    }
}

Our final handler deals with the case that a user has been banned from being a VIP (maybe they stole too many tiny tubes of toothpaste, or had one two many Laphroaigs). Even if other requirements are met, we don't want to grant the authenticated user VIP status. So even if the user is a CEO, has a VIP Number and is an employee - if they are banned, they can't come in.

We can code this business requirement by calling the context.Fail() method as appropriate within the HandleRequirementAsync method:

public class IsBannedAuthorizationHandler : AuthorizationHandler<IsVipRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsVipRequirement requirement)
    {
        if (context.User.HasClaim(claim => claim.Type == "IsBannedFromVIP"))
        {
            context.Fail();
        }
        return Task.FromResult(0);
    }
}

Calling Fail() overrides any other Success() calls for a requirement. Note that whether a handler calls Success or Fail, all of the registered handlers will be called. This ensures that any side effects (such as logging etc) will always be executed, no matter the order in which the handlers run.

Wiring it all up

Now we have all the pieces we need, we just need to wire up out policy and handlers. We modify the configuration of our AddAuthorization call to use our IsVipRequirement, and also register our handlers with the dependency injection container. We can use singletons here as we are not injecting any dependencies.

public void ConfigureServices(IServiceCollection services)  
{
    services.AddMvc();

    services.AddAuthorization(options =>
    {
        options.AddPolicy(
            "CanAccessVIPArea",
            policyBuilder => policyBuilder.AddRequirements(
                new IsVipRequirement("British Airways"));
    });

   services.AddSingleton<IAuthorizationHandler, IsCEOAuthorizationHandler>();
   services.AddSingleton<IAuthorizationHandler, HasVIPNumberAuthorizationHandler>();
   services.AddSingleton<IAuthorizationHandler, IsAirlineEmployeeAuthorizationHandler>();
   services.AddSingleton<IAuthorizationHandler, IsBannedAuthorizationHandler>();
}

An important thing to note here is that we are explicitly creating an instance of the IsVipRequirement to be associated with this policy. That means the "CanAccessVIPArea" policy only applies to "British Airways" employees. If we wanted similar behaviour for "American Airlines" employees, we would need to create a second Policy. It is this IsVipRequirement object which is passed to the HandleRequirementAsync method in our handlers.

With our policy in place, we can easily apply it in multiple locations via the AuthorizeAttribute and protect our Action methods:

public class VIPLoungeControllerController : Controller
{
    [Authorize("CanAccessVIPArea")]
    public IActionResult ViewTheFancySeatsInTheLounge()
    {
       return View();
    }

Applying a global authorisation requirement

As well as applying the policy to individual Actions or Controllers, you can also apply policies globally to protect all of your MVC endpoints. A classic example of this is that you always want a user to be authenticated to browse your site. You can easily create a policy for this by using the RequireAuthenticatedUser() method on PolicyBuilder, but how do you apply the policy globally?

To do this you need to add an AuthorizeFilter to the global MVC filters as part of your call the AddMvc(), passing in the constructed Policy:

services.AddMvc(config =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

As shown in the previous post, the AuthorizeFilter is where the authorisation work happens in an MVC application, and is added wherever an AuthorizeAttribute is used. In this case we are ensuring an additional AuthorizeFilter is added for every request.

Note that as this happens for every Action, you will need to decorate your Login methods etc with the AllowAnonymous attribute so that you can actually authenticate and browse the rest of the site!

Summary

In this post I showed in more detail how authorisation policies, requirements and handlers work in ASP.NET Core. I showed how you could use a Func<> to handle simple policies, and how to create custom requirements and handlers for more complex policies. Finally, I showed how you could apply a policy globally to your whole MVC application.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?