ASP.NET Core cookbook · Intermediate

Protect APIs with JWT bearer tokens and policies

The API must accept access tokens from an identity provider and enforce scopes and roles.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Configure AddJwtBearer with the provider's authority and audience, define named policies for scopes and roles, and require authentication by default with a fallback policy.

Solution

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Auth:Authority"];   // e.g. https://login.example.com/
        options.Audience = builder.Configuration["Auth:Audience"];     // e.g. api://orders
        options.MapInboundClaims = false;                              // keep "sub", "scope" claim names
        options.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(30);
    });
builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())
    .AddPolicy("orders:read", p => p.RequireAssertion(ctx => HasScope(ctx.User, "orders:read")))
    .AddPolicy("orders:write", p => p.RequireAssertion(ctx => HasScope(ctx.User, "orders:write")))
    .AddPolicy("admin", p => p.RequireClaim("roles", "admin"));
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/health", () => "ok").AllowAnonymous();
app.MapGet("/api/orders", () => Array.Empty<object>()).RequireAuthorization("orders:read");
app.MapPost("/api/orders", () => Results.Created()).RequireAuthorization("orders:write");
app.MapDelete("/api/orders/{id}", (string id) => Results.NoContent()).RequireAuthorization("admin");
app.Run();

static bool HasScope(System.Security.Claims.ClaimsPrincipal user, string scope) =>
    user.FindAll("scope").SelectMany(c => c.Value.Split(' ')).Contains(scope);

Pitfalls

  • Skipping audience validation, so tokens issued for other APIs are accepted.
  • Treating ID tokens as access tokens.
  • Checking ownership only with roles; resource-level checks still need IAuthorizationService.

Variations

  • For browser apps, a backend-for-frontend with cookie auth avoids tokens in JavaScript.
  • Microsoft.Identity.Web adds Entra ID specific helpers.

Further reading: Configure JWT bearer authentication

Reference: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication

More ASP.NET Core recipes

Was this page helpful?