Coding Stephan

Configure JWT Authentication in Dotnet

When you develop an API you probably want an easy way to authenticate users. In this post, I will show you how to configure JWT authentication in a .NET API. This is a simple and effective way to secure your API endpoints. What is JWT? And how do you configure it the right way?

What is JWT?

JWT (Json Web Token) is a compact, URL-safe means of representing claims to be transferred between two parties. These claims are then digitally signed, so the recipient can verify the authenticity of the claims. JWT by itself has nothing to do with authentication, but it is often used as a means of transmitting authentication information. JWT is a standard (RFC 7519) and is widely used in web applications.

Want to know more about JWT? Check out my post from 2019: JWT: Part 1.

Prerequisites

This post will assume you already have a working .NET api (ASP.NET Core v6 or higher). You’ll need to add Microsoft.AspNetCore.Authentication.JwtBearer to your project. Use the package manager and pick the appropriate version for your project, 8.0.15 for .NET 8, 9.0.4 for .NET 9, etc.

I would say to never start signing your own JWTs, but instead use a trusted authentication provider, like Microsoft Entra ID or any of the others that provide similar services. More details here

The IDP that you’ll be using will be called the issuer, to auto configure it in our api, you’ll need the Issuer URL (or Authority). Test out the issuer url in your browser by appending .well-known/openid-configuration to the base url. For example:

This should return a JSON document with the issuer information in an openid connection configuration format. The most important information (for us) is the jwks_uri property, which contains the public keys that are used to verify the signature of the JWT and the issuer. These are standard properties in the openid configuration document.

{
  "issuer": "https://login.microsoftonline.com/df68aa03-48eb-4b09-9f3e-8aecc58e207c/v2.0",
  "jwks_uri": "https://login.microsoftonline.com/df68aa03-48eb-4b09-9f3e-8aecc58e207c/discovery/v2.0/keys",
  ...
}

Library quicks

As you see in the sample above the issuer in the configuration document is not the same as the url that I’m configuring for the Authority. Some libraries validate the token based on the Authority, and others read the issuer from the configuration to validate the token. In my opinion both options make sense, reading everything from the configuration document is the most flexible option. As it would help in migration scenarios.

For instance if I would redirect requests from https://svrooij.io/.well-known/openid-configuration to https://login.microsoftonline.com/svrooij.io/v2.0/.well-known/openid-configuration, libraries that use the issuer from the configuration would still work even if I were to switch to a different IDP.

As Microsoft uses redirection from https://login.microsoftonline.com/{tenantIdOrAnyRegisteredDomain}/v2.0/.well-known/openid-configuration to https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration it seems reasonable that they use the issuer from the configuration document and not the Authority to validate the token.

Configuration

As with all configuration, you don’t want to put hard-coded urls in your code, instead we’ll use the built-in configuration system in .NET. This allows you to use appsettings.json, environment variables, or any other configuration provider you want.

Add the JWT section to your appsettings.json file, and change the Authority to the appropriate issuer url. The TokenValidationParameters section is optional. You can set the ValidAudiences property to a list of valid values for the aud claim, if you set this, it will automatically be used to validate this claim.

{
  "JWT": {
    "Authority": "https://login.microsoftonline.com/svrooij.io/v2.0",
    "TokenValidationParameters": {
      "ValidateIssuer": true,
      "ValidAudiences": [
        "...",
      ],
    }
  }
}

Authentication in Program.cs

In your Program.cs file, add the following code to configure JWT authentication. This will add the JWT authentication middleware to the pipeline and configure it to use the settings from the appsettings.json file.

using Microsoft.AspNetCore.Authentication.JwtBearer;
...
var builder = WebApplication.CreateBuilder(args);

// Add this code to configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
  {
    // This is the magic line that connects the provided configuration and binds it to a strongly typed object
    builder.Configuration.Bind("JWT", options);
    
    // Prevent some settings from being overridden by the configuration 
    options.TokenValidationParameters = new TokenValidationParameters
    {
      ValidateIssuer = true
    };

    // For security reasons, you might want to add this line to prevent loading the issuer details over insecure http.
    // options.RequireHttpsMetadata = true;
  });

// Rest of your program
var app = builder.Build();

Explore the JwtBearerOptions to see what else it has to offer.

Authorization

Now that we have the authentication configured we need to add some authorization. Starting with .NET8 this has to be added separately. Add the following code to your Program.cs to configure authorization.

using Microsoft.AspNetCore.Authorization;
...

// Add this code to configure authorization, you can also configure additional policies here.
builder.Services.AddAuthorization(options => {
  options.DefaultPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();
});

// Rest of your program
var app = builder.Build();

// Add this code to enable authentication and authorization
app.UseAuthentication();
app.UseAuthorization();

Extra authorization rules

You can add extra authorization rules to your application by adding them to the AuthorizationPolicyBuilder. For example, if you want all tokens to have a specific scope, you can add the following code to your Program.cs:

builder.Services.AddAuthorization(options => {
  options.DefaultPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .RequireClaim("scp", "api://{your_api_id}/access_as_user")
    .Build();
});

Or if you want to dynamically evaluate the claims in the token, you might add the RequireAssertion method to the AuthorizationPolicyBuilder. This will allow you to add custom authorization rules based on the claims in the token. I would not recommend this for performance reasons, but it is possible. More advances authorization stuff can be done with the IAuthorizationHandler interface.

builder.Services.AddAuthorization(options => {
  options.DefaultPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .RequireAssertion(context =>
    {
      // Check if the token has a specific claim
      return context.User.HasClaim(c => c.Type == "scp" && c.Value == "api://{your_api_id}/access_as_user");
    })
    .Build();
});

Securing your endpoints

Now that the hard work is done, you can secure your endpoint by adding the .RequireAuthorization() method to your endpoint in case of minimal apis.

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi()
// Add the next line to apply the default authorization policy
.RequireAuthorization();

Or with the [Authorize] attribute in case of controllers.

[ApiController]
[Route("[controller]")]
[Authorize] // Add this line to apply the default authorization policy
....

Testing

Configuring authentication will mess up your testing in Swagger, since your endpoint now requires a valid token. More on this topic in the next post.

Provider specific configuration

Some identity providers have additional configuration options that you might want to consider. If you have other providers that have special recommendations, please let me know and I will add them to this post.

If you however use provider specific configuration, be sure to document is for your fellow (or future) developers. For instance not all identity providers support the acr claim, if you use that to make authentication decisions, be sure to document that in your code.

Microsoft Entra ID

If you use this library with Microsoft Entra ID, be sure to validate both the audience and the scope or roles. Each application in that tenant is able to get a token for each application in that tenant. A token will be issued without any roles or scopes present, so just validating the aud claim is not enough. Microsoft is aware of this issue and will stop issuing tokens March 2026 to applications without access to the request api.

Microsoft Entra ID (multi tenant)

If you want to use Microsoft Entra ID in a multi-tenant scenario, you need to set the the following:

  • JWT.Authority to https://login.microsoftonline.com/common/v2.0 (Personal and work accounts) or https://login.microsoftonline.com/organizations/v2.0 (Work accounts only)
  • JWT.TokenValidationParameters.ValidateIssuer property to false. for all tenants or
  • JWT.TokenValidationParameters.ValidIssuers property to a list of valid issuers. This is a list of all the tenants that are allowed to access your API.

Conclusion

Configuring authentication in your dotnet core api is not super complicated. If you use custom claims for authorization decisions, be sure to document them for your fellow developers. If you use a specific identity provider, be sure to check their documentation for any additional configuration options. If you have any questions or comments, please let me on LinkedIn.