✓
Home›Tutorials›.NET & ASP.NET Core›ASP.NET Core 10 Authentication and Authorization: Secure Your Minimal API with JWT Bearer Tokens
.NET & ASP.NET Core6 min read

ASP.NET Core 10 Authentication and Authorization: Secure Your Minimal API with JWT Bearer Tokens

Add JWT bearer authentication and policy-based authorization to an ASP.NET Core 10 Minimal API, then test protected endpoints safely in development.

ASP.NET Core 10 Authentication and Authorization: Secure Your Minimal API with JWT Bearer Tokens

Lesson 4 of the .NET & ASP.NET Core series. Your API now has configuration, dependency injection, and persistent data. The next boundary is deciding who may call an endpoint and what they may do after they arrive.

In this lesson, you will configure JWT bearer authentication for a Minimal API, apply named authorization policies, protect a route group, and test the result with a development token. The goal is a small, understandable baseline—not a home-grown identity provider.

Prerequisites

  • Complete Lesson 3: EF Core with SQLite, or have a .NET 10 Minimal API with working routes.
  • Install the .NET 10 SDK and be comfortable with configuration and DI from Lesson 2.
  • Know that production access tokens should come from a trusted OpenID Connect or OAuth 2.0 provider.

Working outcome

At the end, GET /api/tasks requires any authenticated user, while POST /api/tasks requires a tasks.write scope. The health endpoint remains public. A bearer token that is missing, expired, signed by the wrong issuer, or intended for another audience is rejected.

Authentication versus authorization

Authentication establishes the caller identity. Authorization evaluates whether that caller meets a requirement for a resource. Keep those responsibilities separate: a valid token is not automatically permission to write data.

For an API that accepts JWT access tokens, the API validates the token; it should not manufacture tokens from user passwords. Microsoft recommends standard OIDC/OAuth flows for token acquisition. In production, configure a trusted authority and an audience for your API rather than embedding signing keys in application code.

1. Add the JWT bearer package

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Use a package version compatible with your target framework. The package provides the handler that reads a bearer token from the request and validates it using the authority’s metadata and signing keys.

2. Put identity settings in configuration

Replace these placeholders with values issued by your identity provider. Do not place client secrets or private signing keys in appsettings.json. For a deployed service, use the platform’s secret store and least-privileged configuration access.

{
  "Authentication": {
    "Authority": "https://login.example.com/tenant-id/v2.0",
    "Audience": "api://tasks-api"
  }
}

3. Register authentication and authorization

Set JWT bearer as the default scheme, then add policies that express the API’s rules. This sample intentionally checks a scope claim. Your provider may use scp, scope, roles, or a custom claim—inspect a real access token and write the policy for its documented claim shape.

using Microsoft.AspNetCore.Authentication.JwtBearer;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Authentication:Authority"];
        options.Audience = builder.Configuration["Authentication:Audience"];
        options.MapInboundClaims = false;
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("tasks.read", policy =>
        policy.RequireAuthenticatedUser())
    .AddPolicy("tasks.write", policy =>
        policy.RequireAssertion(context =>
            context.User.FindAll("scp")
                .SelectMany(claim => claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries))
                .Contains("tasks.write")));

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/health", () => Results.Ok(new { status = "ok" }))
   .AllowAnonymous();

var tasks = app.MapGroup("/api/tasks").RequireAuthorization("tasks.read");

tasks.MapGet("/", async (TaskDbContext db, CancellationToken cancellationToken) =>
    await db.Tasks.OrderBy(task => task.Id).ToListAsync(cancellationToken));

tasks.MapPost("/", async (CreateTaskRequest input, TaskDbContext db, CancellationToken cancellationToken) =>
{
    var task = new TodoTask { Title = input.Title.Trim(), IsComplete = false };
    db.Tasks.Add(task);
    await db.SaveChangesAsync(cancellationToken);
    return Results.Created($"/api/tasks/{task.Id}", task);
}).RequireAuthorization("tasks.write");

app.Run();

Minimal API applications can automatically add the authentication and authorization middleware when the corresponding services are registered. Calling UseAuthentication and UseAuthorization explicitly makes the pipeline and its order obvious—particularly useful once you add CORS or custom middleware.

4. Understand what the handler validates

The bearer handler needs to establish trust before it creates a principal. With an authority and audience configured, validate the token signature, issuer, audience, and expiration. Do not turn off signature validation to make a local test pass. A token that is cryptographically valid for a different API is still not valid for this API.

Notice that this API authorizes access tokens, not ID tokens. An ID token describes the authentication event for a client; it is not the API credential. Keep access tokens out of browser storage when a server-side web application can use secure, HTTP-only session cookies instead.

5. Test in development with dotnet user-jwts

For a local-only learning setup, the SDK can create a development JWT. This is useful for exercising your authorization rules; it is not a production identity system. Start the API, then create a token whose audience matches your configured development audience and whose scope claim matches the policy.

dotnet user-jwts create --audience api://tasks-api --claim scp=tasks.write

curl -i https://localhost:5001/api/tasks

curl -i https://localhost:5001/api/tasks \
  -H "Authorization: Bearer YOUR_DEVELOPMENT_TOKEN"

Expected results: the first protected request receives an authentication challenge (normally 401); the second can read tasks if the token is valid. A valid token without tasks.write should receive 403 when posting. Testing both outcomes proves that authentication and authorization are not being confused.

Common mistakes

Trusting a token because it “looks like a JWT”

A JWT is just a token format. Do not parse it in client code and treat its claims as trusted. The API’s authentication handler must validate its signature and expected issuer, audience, and lifetime.

Protecting every route by accident

A fallback policy can require authentication for endpoints without authorization metadata. That is useful in some APIs, but explicitly mark health, login-callback, or documentation endpoints anonymous when they truly need public access. Review each exception.

Putting authorization in a client

Hiding a UI button improves usability, not security. The API must enforce the rule on every protected endpoint.

Using a long-lived token in logs or source control

Treat access tokens as secrets. Use short lifetimes, redact authorization headers from logs, and revoke or rotate credentials exposed during development.

Validation checklist

  1. Call /health without a token and confirm it is public.
  2. Call GET /api/tasks without a token and confirm a 401 response.
  3. Call it with a valid development token and confirm a 200 response.
  4. Call POST /api/tasks with a valid token that lacks tasks.write and confirm a 403 response.
  5. Call the POST with the scope and confirm a 201 response, then verify the row exists through the EF Core-backed endpoint.
  6. Repeat with an incorrect audience to confirm it is rejected.

Where this fits in the track

This lesson builds on the persistent API from Lesson 3: EF Core with SQLite and belongs to the .NET & ASP.NET Core learning track. Keep the track bookmarked as the lessons build into one production-minded API.

Next lesson

Next, you will add input validation and a consistent error contract so invalid requests fail predictably before they reach your data layer.

Sources