/images/resources/code-examples/code-examples-dotnet.jpg

Securing a .NET API with JWTs

On this page

This tutorial explains how to secure API endpoints with access tokens in the JSON web token (JWT) format and implement claims-based authorization. The example API uses the .NET JWT Bearer Middleware and .NET Policy-based Authorization.

Note

The example uses the Curity Identity Server, but you can run the code against any standards-based authorization server.

The API implements the three main stages of API authorization.

  • Validating a JWT access token according to best practices.
  • Verifying that the access token has the scope(s) the API requires.
  • Restricting access to data using claims in the access token.

Project Setup

First install a .NET SDK on version 8.0 or later. Use the following steps to create a .NET API project with the required security libraries, or clone the GitHub repository at the top of this page. If required, add the --version parameter to the dotnet add package commands to use a particular version of the libraries.

bash
1234
dotnet new webapi -o demo
cd demo
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect

To run the API use the build and run commands. Set the .NET environment variable if you want to run in development mode, e.g., when using HTTP URLs.

bash
123
export ASPNETCORE_ENVIRONMENT='Development'
dotnet build
dotnet run

Configuration

The API can use an appsettings.json file to configure OAuth settings and logging settings. Follow JWT Best Practices and configure the expected issuer, audience and token signing algorithm in the settings. The Microsoft.AspNetCore.Authentication and Microsoft.AspNetCore.Authorization loggers output logging events that can be useful to understand the reasons for any authentication and authorization failures.

json
12345678910111213141516
{
"Authorization": {
"Issuer": "http://login.example.com:8443/oauth/v2/oauth-anonymous",
"Audience": "demo-api",
"Algorithm": "RS256"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.Authentication": "Information",
"Microsoft.AspNetCore.Authorization": "Information"
}
}
}

Security Library Integration

To integrate .NET API authentication and authorization, start by adding the services.

csharp
123456789101112131415
public void ConfigureServices(IServiceCollection services)
{
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
...
});
services
.AddAuthorization(options =>
{
...
});
}

Then, instruct .NET to use those services, and make sure you configure authentication before authorization.

csharp
123456789
public void Configure(IApplicationBuilder app)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
}

JWT Access Token Validation

To implement JWT access token validation and authenticate requests, supply the expected issuer, audience and algorithm to the middleware. Disable the MapInboundClaims logic to use OAuth claim names and avoid some claims being translated to Microsoft-specific claim names later.

csharp
123456789
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.MapInboundClaims = false;
options.Authority = Configuration["Authorization:Issuer"];
options.Audience = Configuration["Authorization:Audience"];
options.TokenValidationParameters.ValidAlgorithms = [Configuration["Authorization:Algorithm"]];
});

Use Scopes and Claims for Authorization

When JWT validation succeeds, .NET creates a ClaimsPrincipal object from the access token payload, to use for authorization. You can use Microsoft's policy-based authorization to inspect the ClaimsPrincipal and apply authorization rules.

Start with a fallback policy so that all endpoints require JWT authentication. Then validate scopes and claims. The has_required_scope policy in the example, for instance, verifies that the access token contains a read scope. Access token scopes are space-separated strings, so the policy converts scopes to an array and checks if the required value exists in the array.

The example authorization also uses a has_low_risk policy that verifies a custom numeric claim called risk has a value less than 50. This authorization rule uses a claim that might originate from a risk engine that examines behavior patterns. The authorization server issues the claim to an access token so that the API receives it.

csharp
1234567891011121314151617181920
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
options.AddPolicy("has_required_scope", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(claim =>
claim.Type == "scope" && claim.Value.Split(' ').Any(c => c == "read")
)
)
);
options.AddPolicy("has_low_risk", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(claim =>
claim.Type == "risk" && Int32.Parse(claim.Value) < 50
)
)
);
});

More generally, issue any custom Scopes and Claims to access tokens that APIs need for authorization purposes. Then, compose policies that use those scopes and claims to implement authorization rules. You can implement complex rules with Custom Authorization Handlers.

Assign Policies to Endpoints

To complete the OAuth authorization, assign policies to controller endpoints. The example API uses one endpoint that represents medium sensitivity data, which only requires the scope-based policy. Another endpoint represents high sensitivity data that uses both the scope-based policy and the claims-based policy.

csharp
12345678910111213141516171819
[ApiController]
[Route("[controller]")]
public class DemoController : ControllerBase
{
[HttpGet("data")]
[Authorize(Policy = "has_required_scope")]
public IActionResult MediumSensitivityData()
{
return Ok(new { data = "Some medium sensitivity data", user = GetSubject() });
}
[HttpGet("highworthdata")]
[Authorize(Policy = "has_required_scope")]
[Authorize(Policy = "has_low_risk")]
public IActionResult HighSensitivityData()
{
return Ok(new { data = "Some high sensitivity data", user = GetSubject(), risk = GetClaim("risk") });
}
}

Test the API

To test the API you need an authorization server and a test client that delivers a JWT access token to the API. Use the Getting Started Guides to get up and running, then run commands to send access tokens to the two endpoints.

bash
123
ACCESS_TOKEN='eyJ0e...aOCg'
curl -i http://localhost:5000/demo/data -H "Authorization: Bearer $ACCESS_TOKEN"
curl -i http://localhost:5000/demo/highworthdata -H "Authorization: Bearer $ACCESS_TOKEN"

If you supply an invalid access token that fails validation, the request returns an error response with a 401 unauthorized status code, as in the following example. Similarly, if the token does not have the required scopes or claims, the API denies access with a 403 forbidden response.

text
12345
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Thu, 15 May 2025 17:07:48 GMT
Server: Kestrel
WWW-Authenticate: Bearer

Conclusion

Protecting .NET API endpoints with JWT access tokens require is straightforward once the JWTs contain the right scopes and claims, since the .NET authentication and authorization middleware reduces code. Developers can also use the techniques in the Testing Zero Trust APIs example for a productive way to call their APIs. For further information on API authorization and token design, see the Scope Best Practices and Claims Best Practices articles.

Newsletter

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Newsletter

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial

Was this helpful?