How to Authorize Requests in ASP.NET Core Web API

Introduction – What is Authorization?

The word Authorization comes from the verb Authorize, which means “give official permission for or approval to (an undertaking or agent)”.

Authorization is the process of validating if a person is allowed to do what the person is requesting to do and taking respective action.

Authorization with an example

Let’s say you have logged into your Facebook account and are scrolling through your timeline. All the actions that you do on your timeline correspond to your own account. You can like someone’s post or share it onto your own timeline. But you cannot see an option to edit someone else’s post or content.

That is because Facebook deems you are not Authorized to perform any action on content that is created by someone else.

But you can still create and manage your own posts or content and others cannot take any action on your own content (except for reacting to it). This is an example of how Authorization internally works in almost all the applications.

Authorization for Acess Control

With Authorization, you can build systems that have permission-based or entitlements-based functionalities or features. That means you can restrict people from performing or accessing certain sections of your application, while allowing only those who are authorized to do so.

Think of how all those Pro features in applications know why you should not be allowed to access premium unless you have paid for it.

Differences between Authentication and Authorization

The word that comes closest to Authorization is Authentication.

Authentication is Verifying Identity

Authentication is the process of validating and proving one’s identity with respect to a system. It means to prove that you are, who you are, the system is expecting to be. Authentication takes many forms – Email Password, Username Password, Biometric, MFA etc.

Authorization is the next step to Authentication. For a user to be authorized to do something, the user must first be authenticated.

Authorization needs Authentication

A System that has an Authentication mechanism in place may or may not have an Authorization mechanism for its users. But a system that has an Authorization mechanism MUST Authenticate its users first.

Authentication is Identity Management. Authorization is Access Control.

Authorization Middleware in ASP.NET Core

In ASP.NET Core, you can implement Authorization using the built-in Authorization middleware and library provided by the framework.

You need to configure and add the services to the service pipeline. Once your application starts receiving requests, the Authorization middleware is invoked for all requests marked to be Authorized and checks for the requirements to satisfy.

The Authorization middleware acts after the Authentication middleware, so it expects that the request has an Authenticated User Context. It checks within the Claims that the Context has and validates against the requirements.

If everything is satisfied, the middleware allows the request to be processed. Otherwise the middleware short circuits the request and returns a 403 Forbidden response back to the client.

How to implement simple Authorization in ASP.NET Core with an Example

Let us see how to add Authorization to our ASP.NET Core Web API. To demonstrate, I will create a new WebAPI application and add the middleware. Let us start by using the following dotnet CLI command –

> dotnet new webapi --name BookStore.WebAPI

It creates a new WebAPI project, with a default WeatherForecastController and model. We will remove the unwanted Controller and create a new Controller named BooksController.

The BooksController has two API endpoints – GetTitles and AddTitle

GetTitle is a normal endpoint that returns a collection of all titles available in the system. It is open and available for all the users to access irrespective of their login state.

AddTitle is an Admin action that adds a new Title to the system. It is not meant for everyone to access and is a Closed API that requires an authenticated and authorized user to invoke. We will apply Authorization functionality on the AddTitle endpoint.

using BookStore.Authorization.Requirements.Handlers;
using BookStore.Managers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BookStore.WebAPI.Controllers;

[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
    private List<string> _titles = new List<string> {
        "The Alchemist",
        "To kill a Mockingbird",
        "The Daily Stoic",
        "Winner Stands Alone",
        "The Hitchhiker's Guide to the Galaxy" };


    [HttpGet("GetTitles")]
    public IEnumerable<string> GetTitles()
    {
        return _titles.ToArray();
    }


    [Authorize]
    [HttpPost("AddTitle")]
    public async Task AddTitle([FromForm] string titleName)
    {
        _titles.Add(titleName);

        Console.WriteLine($"Added Title - {titleName}");
        await Task.CompletedTask;
    }
}

Authorization middleware is invoked on the controllers which are decorated with the Authorize attribute.

Hence the AddTitle endpoint is decorated with the Authorize attribute, while the other endpoint is not – because it doesn’t need to be authorized.

We need to configure the Authorization service and the middleware with the necessary requirements for a request to be deemed authorized. In the Program.cs class, we will add the middleware as below –

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddAuthentication();

// Register the Authorization service
builder.Services.AddAuthorization();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();

// Add the Authorization middleware
// to the request pipeline
app.UseAuthorization();

app.MapControllers();

app.Run();

3 Ways of Handling Authorization in ASP.NET Core

The above code gives you an idea of how to add authorization middleware and service to the request pipeline. This doesn’t actually complete the Authorization setup. Observe that we have not yet mentioned anywhere, the requirements that deem an incoming request as authorized.

There are 3 ways in which you can configure authorization requirements in ASP.NET Core.

  1. Role based Authorization
  2. Claims based Authorization
  3. Custom Policy based Authorization

Let us briefly discover what each of these mean and how we implement them.

Role based Authorization in ASP.NET Core with an Example

In Role-based Authorization, the Authorization middleware looks for the Roles that are present in the incoming Request User Claims and validates access.

A Claim is a key-value pair created and attached to the User Identity after a successful authentication.

For example, when we are using JWT Token based Authentication where the client passes a JWT along with the request to represent the user, the ASP.NET Core Authentication middleware (that runs before Authorization) parses the token and reads the contents of the payload, which is a JSON object.

It adds all the contents of the JSON document into the Claims and creates a Claims Identity for the User.

The middleware looks for all the values with the key “Role” in the Claims and sees if the request contains Roles that are expected by the Authorization requirement.

If the request contains those Roles, it is Authorized and the endpoint is executed. Else, the middleware returns a 403 Forbidden.

To configure Role-based Authorization, we must pass the Roles that are required to be present. We can pass these Roles as parameters to the Authorize attribute we decorate on top of the endpoints.

For example, if AddToken method can only be called by the users who have an Admin Role, we pass the Role as parameter to the Authorize attribute as follows –

[Authorize(Roles = "Admin")]
[HttpPost("AddTitle")]
public async Task AddTitle([FromForm] string titleName)
{
    _titles.Add(titleName);

    Console.WriteLine($"Added Title - {titleName}");
    await Task.CompletedTask;
}

The Roles argument of the Authorize attribute takes a String value. If we want to pass multiple Roles to the Authorize attribute, we can pass a String that has comma separated Roles.

The middleware splits based on the Comma and validates if the request Claims have any of these Roles matching.

[Authorize(Roles = "Admin,Editor")]
[HttpPost("AddTitle")]
public async Task AddTitle([FromForm] string titleName)
{
    _titles.Add(titleName);

    Console.WriteLine($"Added Title - {titleName}");
    await Task.CompletedTask;
}

The Program.cs file has no change with respect to the Authorization middleware. The Authentication middleware is configured accordingly to support Token Authentication.

Claims based Authorization in ASP.NET Core with an Example

In Claims based Authorization, we authorize a request based on the presence or the validity of a Claim that is present in the Claims Identity.

This approach takes a little-bit of effort but supports customization of our Authorization requirement.

For example, we can set up a requirement to reject a request if it doesn’t contain a Claim “IsAdmin” in its Claims Identity.

To implement Claims-based Authorization, we create an Authorization Policy. A Policy object is a logical structure that contains all the conditions necessary for a request to be authorized.

We can create multiple Policies for multiple scenarios and use them accordingly.

The Authorization service is modified as below –

builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy("ShouldBeAdmin", policy =>
        {
            policy.AddAuthenticationSchemes(
                      JwtBearerDefaults.AuthenticationScheme);
            policy.RequireAuthenticatedUser();
            policy.RequireClaim("IsAdmin", "true");
        }
    }
);

In the above definition, we are creating a Policy called ShouldBeAdmin, where we state that the Claims Identity must Contain a Claim with the Key “IsAdmin” and its value must be “true”.

The AddTitle method is modified as below –

[Authorize(Policy = "ShouldBeAdmin")]
[HttpPost("AddTitle")]
public async Task AddTitle([FromForm] string titleName)
{
    _titles.Add(titleName);

    Console.WriteLine($"Added Title - {titleName}");
    await Task.CompletedTask;
}

Any request that has this Claim is decided to be authorized and allowed to invoke the AddTitle method. Other requests are rejected and returned a 403 Forbidden.

How to handle multiple Requirements for Authorization in ASP.NET Core

Within Claims-based Authorization, we may need to check for two or more claims or apply some condition over them.

In such cases, we can use another method within the AuthorizationPolicy called RequireAssertion. It takes a Function argument that has the AuthorizedUserContext as a parameter and returns a boolean value.

In this function delegate we can add our business logic for authorization and return true/false.

The Policy looks like below –

builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy("ShouldBeAdmin", policy =>
        {
            policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
            policy.RequireAuthenticatedUser();


            policy.RequireAssertion((authHandlerContext) =>
            {
                var claims = authHandlerContext.User.Claims;

                // return true if the Claims contain IsAdmin or has Role Editor
                return claims.Any(
                x => x.Type == "IsAdmin" && x.Value == "true")
                || claims.Any(x => x.Type == "Role" && x.Value == "Editor");
            });
        }
    }
);

Custom Policy based Authorization in ASP.NET Core with an Example

Policy-based Authorization is used when we have complex validation logic and may want to inject and use other services in the Dependency Injection. It can be used in cases where the database is queried for User entitlements and the request is authorized based on the availability of required entries.

Behind a Claims-based or Role-based Authorization, the framework creates an Authorization Requirement and uses the default Authorization Handler to mark the request as authorized or unauthorized.

To create a custom Policy-based Authorization, we first create a Requirement

A Requirement is a logical structure that Asserts an Authorization scenario – say like “UserIsAnAdmin”, “UserHasEditorEntitlements” etc.

We then create an AuthorizationHandler that checks if a user context satisfies the Requirement and decides if the request is allowed or denied

To demonstrate, let us assume we need to create a custom authorization requirement of a User having Admin access. The Requirement is a simple class that implements IAuthorizationRequirement.

The class is as below –

using Microsoft.AspNetCore.Authorization;

namespace BookStore.Authorization.Requirements;

public class ShouldBeAdminRequirement : IAuthorizationRequirement
{
    public ShouldBeAdminRequirement()
    {
    }
}

An Authorization Handler takes a type argument of type IAuthorizationRequirement and decides if the request satisfies this requirement or not. A custom AuthorizationHandler class looks like below –

using Microsoft.AspNetCore.Authorization;

namespace BookStore.Authorization.Requirements.Handlers;

public static class CustomClaimTypes
{
    public const string IS_ADMIN = "IsAdmin";
}

public class ShouldBeAdminRequirementAuthorizationHandler
    : AuthorizationHandler<ShouldBeAdminRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ShouldBeAdminRequirement requirement)
    {
        if (!context.User.HasClaim(x => x.Type == CustomClaimTypes.IS_ADMIN))
            return Task.CompletedTask;

        var isUserAdminClaim = context.User.Claims.First(x => x.Type == CustomClaimTypes.IS_ADMIN).Value;
        bool isUserAnAdmin = bool.Parse(isUserAdminClaim);

        // check if the user
        if (isUserAnAdmin)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Unless we explicitly set the context as Succeed(), the framework treats it unauthorized. We register our custom implementation of the AuthorizationHandler as a singleton Service in the Service pipeline and add the custom requirement in our policy.

using System.Text;
using BookStore.Authorization.Requirements;
using BookStore.Authorization.Requirements.Handlers;
using BookStore.Managers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// custom authorization handler
// registered as a service
builder.Services.AddSingleton<
    IAuthorizationHandler, ShouldBeAdminRequirementAuthorizationHandler>();

builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
        .AddJwtBearer(
            JwtBearerDefaults.AuthenticationScheme,
                (options) => configureBearerOptions(options));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ShouldBeAdmin", policy =>
    {
        policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
        policy.RequireAuthenticatedUser();


        // add the custom requirement to the policy
        policy.Requirements.Add(new ShouldBeAdminRequirement());
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();

app.UseAuthorization();

app.MapControllers();

app.Run();
Authorized Request – Success Response
Change in Requirement – 403 Forbidden Request

Buy Me A Coffee
Enjoyed my content and found it useful?
Consider buying me a coffee to support my work and keep the content flowing!

Conclusion

Authorization is the process of deciding whether a user needs to be allowed to pass or not. It is like your average tollgate on a highway that only lets vehicles with a valid ticket and permit pass through it. It is core to access-control and helps in implementing features to keep unwanted access in check.

This detailed guide introduces us to the 3 ways of Authorization: Role-based, Claims-based and Policy-based Authorization.

Depending on the requirement and complexity, developers can choose from any of these approaches and build authorization mechanisms that help secure Web APIs from unwanted access.

Official Documentation for Reference –

https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-7.0
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims?view=aspnetcore-7.0
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-7.0

Sriram Mannava
Sriram Mannava

A software Engineer from Hyderabad, India with close to 9 years experience as a full stack developer in Angular and .NET space.

Articles: 5