Dynamically Applying Authorization Policies in ASP.NET Core

In some situations, we require multiple authorization methods. For example, when using Single Page Applications (SPA) such as Angular, there might be web APIs that use JWT bearer tokens and MVC login/logout views that use cookie-based authentication.

In this article, we look at how to dynamically apply different authorization policies to different parts of an ASP.NET Core application based on certain criteria.

Authentication is determining who the user is.
Authorization is establishing what a user is allowed to do.

In ASP.NET Core, authorization can be expressed using a policy, which consists of one or more requirements and authentication schemes. We apply a policy using [Authorize("policy")]. However, in some cases this can be error-prone and inefficient. What if we forget to apply a policy where it's needed? Fortunately, we can automate this procedure in many ways, one of which is using conventions.

Let's assume that we have two different authorization requirements for our application:

  1. Web API
    • Require user to be authenticated
    • Use JWT bearer tokens
  2. Default
    • Require user to be authenticated
    • Use cookie-based authentication

It's a good practice to require authentication globally. This will prevent us from inadvertantly allowing access to anonymous users by forgetting to apply the [Authorize] attribute. We can use [AllowAnonymous] in places where authentication is not needed.

We can express these in code as follows:

services.AddAuthorization(options =>
{
    options.AddPolicy("defaultpolicy", builder =>
    {
        builder.RequireAuthenticatedUser();
    });

    options.AddPolicy("apipolicy", builder =>
    {
        builder.RequireAuthenticatedUser();
        builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
    });
});

We can now apply these policies with [Authorize("defaultpolicy")] and [Authorize("apipolicy")].

Next, we define a convention that applies these policies everywhere in our application. Typically, web API controllers have an attribute that defines a route that starts with "api", such as [Route("api/[controller]")]. Based on this, we can selectively apply one of the pre-defined policies to each controller.

public class AuthorizationPolicyConvention : IApplicationModelConvention
{
    private string _defaultPolicy;
    private string _apiPolicy;

    public AuthorizationPolicyConvention(string defaultPolicy, string apiPolicy)
    {
        _defaultPolicy = defaultPolicy;
        _apiPolicy = apiPolicy;
    }

    public void Apply(ApplicationModel application)
    {
        foreach (var controller in application.Controllers)
        {
            var isApiController = controller.Selectors.Any(x => x.AttributeRouteModel != null &&
                                                                x.AttributeRouteModel.Template.StartsWith("api"));

            if (isApiController)
            {
                controller.Filters.Add(new AuthorizeFilter(_apiPolicy));
            }
            else
            {
                controller.Filters.Add(new AuthorizeFilter(_defaultPolicy));
            }
        }
    }
}

Finally, we add our convention to the conventions list like so:

services.AddMvc(options =>
{
    options.Conventions.Add(new AuthorizationPolicyConvention("defaultpolicy", "apipolicy"));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

With this in place, our authorization policies are applied to every controller in our application. Web APIs will have the "apipolicy", and all the rest the "defaultpolicy".

Useful Resources

Jon Turdiev

Technology professional with over 15 years of experience in systems architecture, hardware and software development, project management, and technical support. Specializing in industrial IoT and Cloud solutions.