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.
dotnet new webapi -o democd demodotnet add package Microsoft.AspNetCore.Authentication.JwtBearerdotnet 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.
export ASPNETCORE_ENVIRONMENT='Development'dotnet builddotnet 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.
{"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.
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.
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.
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.
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.
[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.
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.
HTTP/1.1 401 UnauthorizedContent-Length: 0Date: Thu, 15 May 2025 17:07:48 GMTServer: KestrelWWW-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.
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial