JWT Bearer Authentication and Authorization for ASP.NET Core 5
Tags: aspnetcore, dotnetcore, auth0, jwt
Published: 2020-12-18 14:35:17

---
title: JWT Bearer Authentication and Authorization for ASP.NET Core 5
published: true
date: 2020-12-18 00:00:00 UTC
tags: aspnetcore, dotnetcore, auth0, jwt
canonical_url: https://sandrino.dev/blog/aspnet-core-5-jwt-authorization
---

## Introduction

On [November 10th, 2020](https://devblogs.microsoft.com/aspnet/announcing-asp-net-core-in-net-5/) Microsoft released .NET 5 and the updated ASP.NET Core platform which includes a long list of performance improvements.

![.NET 5 Platform](https://sandrino.dev/assets/blog/aspnet-core-5-jwt-authorization/dotnet5.jpeg)

In this article we'll cover how you can configure JWT Bearer authentication and authorization for APIs built with ASP.NET Core 5. There are plenty of resources out which cover how to build your own "JWT authentication" with symmetric signing, but in this article we'll be focussing on leveraging OpenID Connect and OAuth 2 flows (using Auth0/Identity Server/Okta/...) where APIs are protected resources. Let's first take a look at how all pieces fit together from a high level. The APIs you build are typically called **by applications on the user's behalf** or **on their own behalf**.

Users interact with a SPA/Mobile App/Desktop App/Web Application/CLI/... and will be authenticating using OpenID Connect (Authorization Code Grant). The authorization server will issue an `id_token` (used by the application to authenticate the user) and an `access_token` which is used by the application to call the API **on the users behalf**.

![User Authentication](https://sandrino.dev/assets/blog/aspnet-core-5-jwt-authorization/user_flow.jpeg)

When applications need to call an API **on their own behalf** they'll use the [OAuth 2.0 Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4) to acquire an `access_token` directly:

![Client Authentication](https://sandrino.dev/assets/blog/aspnet-core-5-jwt-authorization/client_flow.jpeg)

## Configuring JWT Bearer Authentication

We'll start by creating a helper method which will handler all of the JWT Bearer configuration, using the `Microsoft.AspNetCore.Authentication.JwtBearer` package.

```csharp:JwtBearerConfiguration.cs
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

public static class JwtBearerConfiguration
{
  public static AuthenticationBuilder AddJwtBearerConfiguration(this AuthenticationBuilder builder, string issuer, string audience)
  {
    return builder.AddJwtBearer(options =>
    {
      options.Authority = issuer;
      options.Audience = audience;
      options.TokenValidationParameters = new TokenValidationParameters()
      {
        ClockSkew = new System.TimeSpan(0, 0, 30)
      };
      options.Events = new JwtBearerEvents()
      {
        OnChallenge = context =>
        {
          context.HandleResponse();
          context.Response.StatusCode = StatusCodes.Status401Unauthorized;
          context.Response.ContentType = "application/json";

          // Ensure we always have an error and error description.
          if (string.IsNullOrEmpty(context.Error))
            context.Error = "invalid_token";
          if (string.IsNullOrEmpty(context.ErrorDescription))
            context.ErrorDescription = "This request requires a valid JWT access token to be provided";

          // Add some extra context for expired tokens.
          if (context.AuthenticateFailure != null && context.AuthenticateFailure.GetType() == typeof(SecurityTokenExpiredException))
          {
            var authenticationException = context.AuthenticateFailure as SecurityTokenExpiredException;
            context.Response.Headers.Add("x-token-expired", authenticationException.Expires.ToString("o"));
            context.ErrorDescription = $"The token expired on {authenticationException.Expires.ToString("o")}";
          }

          return context.Response.WriteAsync(JsonSerializer.Serialize(new
          {
            error = context.Error,
            error_description = context.ErrorDescription
          }));
        }
      };
    });
  }
}
```

In the code above we're configuring the `AddJwtBearer` method with the following:

- `Authority`: The issuer (eg: `https://sandrino.auth0.com/`)
- `Audience`: Typically the identifier of your API which has been registered at the authorization server (eg: `http://my-api`)
- `ClockSkew`: This is set to 5 minutes by default, but 30 seconds should be fine in most cases. This is to handle any differences in time between the authorization server and the API.

We're also modifiying the response of any JWT validation error to return a JSON object instead of the standard `WWW-Authenticate` challenge. This is optional and provides your clients with more context which can be useful to handle the error.

The helper can now be used to register an authentication service in the `Startup` class:

```csharp{15,16,17,18,19}:Startup.cs
public class Startup
{
  private readonly IConfiguration _configuration;

  public Startup(IConfiguration configuration)
  {
    _configuration = configuration;
  }

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

    // Configure JWT authentication.
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
      .AddJwtBearerConfiguration(
        _configuration["Jwt:Issuer"],
        _configuration["Jwt:Audience"]
      );
  }

  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  {
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
      endpoints.MapControllers();
    });
  }
}
```

The `Jwt.Issuer` and `Jwt.Audience` settings will be read the `appsettings.json` configuration file:

```json:appsettings.json
{
  "Jwt": {
    "Issuer": "https://sandrino-dev.auth0.com/",
    "Audience": "urn:my-api"
  },
  ...
}
```

And that's it, we can now start creating the necessary APIs and secure them.

## Creating a protected API

Let's start by creating a simple API which returns the claims for the current identity. In the `Get` action we'll use the `[Authorize]` attribute which requires the HTTP request to be authenticated.

```csharp{15}:UserController.cs
public class UserInfo
{
  [JsonPropertyName("id")]
  public string Id { get; set; }

  [JsonPropertyName("claims")]
  public Dictionary<string, string> Claims { get; set; }
}

[ApiController]
[Route("/api/claims")]
public class UserController : ControllerBase
{
  [HttpGet]
  [Authorize]
  public UserInfo Get()
  {
    return new UserInfo()
    {
      Id = this.User.GetId(),
      Claims = this.User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value)
    };
  }
}
```

In order to get the user's identifier using `GetId()` we can write a small helper class:

```csharp:UserHelpers.cs
using System.Security.Claims;

public static class UserHelpers
{
  public static string GetId(this ClaimsPrincipal principal)
  {
    var userIdClaim = principal.FindFirst(c => c.Type == ClaimTypes.NameIdentifier) ?? principal.FindFirst(c => c.Type == "sub");
    if (userIdClaim != null && !string.IsNullOrEmpty(userIdClaim.Value))
    {
      return userIdClaim.Value;
    }

    return null;
  }
}
```

Let's go ahead and start our API. If we call this endpoint without providing a valid `access_token` in the `Authorization` header this will result in the following error:

```json
{
  "error": "invalid_token",
  "error_description": "This request requires a valid JWT access token to be provided"
}
```

We can now try that same request with a valid token in the `Authorization` header:

```bash
curl \
  --request GET 'http://localhost:5001/api/claims' \
  --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InA0UVUtODVUY09GeG03c05JMWlaYyJ9.eyJpc3MiOiJodHRwczovL3NhbmRyaW5vLWRldi5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NTk3YTA2NTExM2Y0MGIwODQ4NWVlN2JkIiwiYXVkIjpbInVybjpteS1hcGkiLCJodHRwczovL3NhbmRyaW5vLWRldi5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNjA4Mjg2OTMxLCJleHAiOjE2MDgyOTY5MzEsImF6cCI6IllRd0Q0YTBBMTFreURJQzJPcVBLNnVDR3FHNEQ3cnVJIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBvZmZsaW5lX2FjY2VzcyIsImd0eSI6InBhc3N3b3JkIn0.l9dOVOXvnFhmMbUAelGiQJTwlCpgXqE6nbrdbTJhg1shxhMiGSuMg3YN3eFLD3-TfU8T5nHNttjgHdlIus-oQuJspYg4Mqu6NTIE0PxGnQQDYqADnXzpLV4OdFc2k1YuZwCpE8dJDJ0lzvXTsio3DKvWq_Vq3gL7qAWtF5EefKbsfTOaLhVPZ8YIcY8C0VSReJnC2M8da0KAdP0SqYJB_BIZYeQiPg668MrGFWsKuQv1h4C9DU3o9Ol0S1nHZ6r8KiiMSQRJyFV7v82VQ3dZWjrj5YWGGR4Uk1Wuf3iochLxRz64MQp-iV_fuE1DECLjKTt6Bj-nLR2PZFDTHAheCA'
```

And this will then return the user's ID and the claims as expected:

```json
{
  "id": "auth0|597a065113f40b08485ee7bd",
  "claims": [
    {
      "name": "iss",
      "value": "https://sandrino-dev.auth0.com/"
    },
    {
      "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
      "value": "auth0|597a065113f40b08485ee7bd"
    },
    {
      "name": "aud",
      "value": "urn:my-api"
    },
    {
      "name": "aud",
      "value": "https://sandrino-dev.auth0.com/userinfo"
    },
    {
      "name": "iat",
      "value": "1608286931"
    },
    {
      "name": "exp",
      "value": "1608296931"
    },
    {
      "name": "azp",
      "value": "YQwD4a0A11kyDIC2OqPK6uCGqG4D7ruI"
    },
    {
      "name": "scope",
      "value": "openid profile offline_access"
    },
    {
      "name": "gty",
      "value": "password"
    }
  ]
}
```

> Note that you can easily test the above using [Auth0 and Insomnia](https://sandrino.dev/blog/testing-auth0-insomnia#authorization-code-grant).

## What just happened?

You might be wondering what just happened?! How was the API able to validate the JWT Bearer token without having to configure a secret or a public key? This is because the authentication service will use the OIDC metadata endpoints to get all of the necessary information.

1. The OpenID Configuration is read first: https://sandrino-dev.auth0.com/.well-known/openid-configuration
2. From there it will find the url to the `jwks_uri` and then load that one: https://sandrino-dev.auth0.com/.well-known/jwks.json
3. The public key(s) are loaded from that document and used to verify the incoming JWT Bearer tokens

## Creating an Authorization Policy

The above is a good step to create a secure API, but it might not be granular enough. Not everyone might have access to all operations that are exposed in your API. This is where you'll want to create an **Authorization Policy** in which you'll be able to restrict access to certain operations.

In our example we'll create an endpoint to query the Billing Settings which is only available to users who have the `read:billing_settings` scope. In your Authorization Server you'll typically configure that only users that are member of a certain group, only users with a specific role or permission ... can receive this scope. But once the application can request that scope on the user's behalf it will be available in the `access_token` and the call to this endpoint will succeed.

The authorization handler implements our business requirement. It will extract the `scope` claim from the current principal and will then validate if the configured claim (eg: `read:billing_settings`) is available. If it is, then the request is allowed to continue.

```csharp:RequireScopeHandler.cs
public class ScopeRequirement : IAuthorizationRequirement
{
  public string Issuer { get; }

  public string Scope { get; }

  public ScopeRequirement(string issuer, string scope)
  {
    Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
    Scope = scope ?? throw new ArgumentNullException(nameof(scope));
  }
}

public class RequireScopeHandler : AuthorizationHandler<ScopeRequirement>
{
  protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ScopeRequirement requirement)
  {
    // The scope must have originated from our issuer.
    var scopeClaim = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer);
    if (scopeClaim == null || String.IsNullOrEmpty(scopeClaim.Value))
      return Task.CompletedTask;

    // A token can contain multiple scopes and we need at least one exact match.
    if (scopeClaim.Value.Split(' ').Any(s => s == requirement.Scope))
      context.Succeed(requirement);
    return Task.CompletedTask;
  }
}
```

We should then register this policy for every scope our API supports and also register the handler:

```csharp{12-16,21}:Startup.cs
// Create an authorization policy for each scope supported by my API.
services.AddAuthorization(options =>
{
  var scopes = new[] {
    "read:billing_settings",
    "update:billing_settings",
    "read:customers",
    "read:files"
  };

  Array.ForEach(scopes, scope =>
    options.AddPolicy(scope,
      policy => policy.Requirements.Add(
        new ScopeRequirement(_configuration["Jwt:Issuer"], scope)
      )
    )
  );
});

// Register our authorization handler.
services.AddSingleton<IAuthorizationHandler, RequireScopeHandler>();
```

> For each scope we register a policy with the name of that scope, allowing us to use `[Authorize("read:billing_settings")]` later in our code.

As a final step we can now create a `BillingController` in which we'll expose the necessary functionality for a user to manage their billing settings. The `/api/billing/settings` endpoint requires the presence of the `read:billing_settings` scope:

```csharp{7}:BillingController.cs
[ApiController]
[Route("/api/billing")]
public class BillingController : ControllerBase
{
  [HttpGet]
  [Route("settings")]
  [Authorize("read:billing_settings")]
  public BillingSettings Get()
  {
    return new BillingSettings()
    {
      Country = "United States",
      State = "Washington",
      Street = "Microsoft Road 1",
      VATNumber = "987654321"
    };
  }
}
```

When interacting with my authorization server I'll want to make sure to request this scope:

```json{3}
https://sandrino-dev.auth0.com/authorize
  ?client_id=5CMfGLsLworduMTOfVD0Kap2IQm4xpLH
  &scope=openid profile read:billing_settings
  &redirect_uri=https://jwt.io
  &response_type=code
  &audience=urn:my-api
  &...
```

And then the resulting `access_token` which now includes the `read:billing_settings` scope can be used to call the endpoint:

```json
GET /api/billing/settings
Host: localhost:5001
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InA0UVUtODVUY09GeG03c05JMWlaYyJ9.eyJpc3MiOiJodHRwczovL3NhbmRyaW5vLWRldi5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NTk3YTA2NTExM2Y0MGIwODQ4NWVlN2JkIiwiYXVkIjpbInVybjpteS1hcGkiLCJodHRwczovL3NhbmRyaW5vLWRldi5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNjA4Mjg3MzY4LCJleHAiOjE2MDgyOTczNjgsImF6cCI6IllRd0Q0YTBBMTFreURJQzJPcVBLNnVDR3FHNEQ3cnVJIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSByZWFkOmJpbGxpbmdfc2V0dGluZ3Mgb2ZmbGluZV9hY2Nlc3MiLCJndHkiOiJwYXNzd29yZCJ9.CMvAa4gVO5dFnXBqWK8UIq5mB--3JacVv9MwocnWFTSR5p938zhw5hMREqUGCesIWy5UUZeb7ka7Dhp4Mf3tK-8h3psfsPMMpI3OP4q8IglKplt1KaXe5rn8Fmm2daNDnxmMccXusLI7T_Ea3hfVrjrfURprNfXW9vCS17Xj6mRHF9RBHNkeg8CKyotatPQojY_uex2L3qBhJhGXBd8CHvnnbEVZMYVlc_D02tqMu4bvs9QCml8y3qQkyvBHOAEJcE7b84trIJK2vIh7B339l-ukeSyK1AEkf5hHAlUjGRuB1dhtfodWLexEd5rH-Tn55xwdvL2CyQI-J2JVIQS0Kw

{
  "country": "United States",
  "state": "Washington",
  "street": "Microsoft Road 1",
  "vat_number": "987654321"
}
```

Calling this endpoint without the required `read:billing_settings` scope will result in a 403 Forbidden.

### Auth0 Role Based Access Control

If you're using Auth0 as your authorization server you can configure the "RBAC authorization policies" for your APIs:

![RBAC](https://sandrino.dev/assets/blog/aspnet-core-5-jwt-authorization/auth0_rbac.jpeg)

This will restrict access to the scopes defined on the API to users who have the required Role or Permission assigned.

![Scopes](https://sandrino.dev/assets/blog/aspnet-core-5-jwt-authorization/scopes.jpeg)

We can now create a Role **Billing Admin** in which we'll add the `read:billing_settings` permission:

![Scopes](https://sandrino.dev/assets/blog/aspnet-core-5-jwt-authorization/permission.jpeg)

And as a final step we can assign the role to our users, allowing applications to request the `read:billing_settings` scope for them.

## ✅ Success!

With all of the above you should be all setup to configure JWT Bearer authentication and authorization in your own APIs.

A full demo application is available on GitHub: [https://github.com/sandrinodimattia/aspnet-core-5-jwt-bearer-demo](https://github.com/sandrinodimattia/aspnet-core-5-jwt-bearer-demo)