Authentication and authorisation
Schemes, policies and requirements, plus the difference between a user being logged in and a user being allowed to touch this row.
Schemes and handlers
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://auth.example.com/";
options.Audience = "https://api.example.com";
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30), // default is five minutes
NameClaimType = "sub",
RoleClaimType = "role",
};
});
// For a browser application, cookies plus anti-forgery
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.Cookie.HttpOnly = true;
o.Cookie.SameSite = SameSiteMode.Lax;
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.SlidingExpiration = true;
o.ExpireTimeSpan = TimeSpan.FromHours(8);
});
builder.Services.AddAuthorization();
builder.Services.AddAntiforgery(o => o.HeaderName = "X-CSRF-TOKEN");
var app = builder.Build();
app.UseAuthentication(); // must come before
app.UseAuthorization(); // ... this one
app.MapGet("/orders", () => Results.Ok()).RequireAuthorization();
app.MapPost("/orders", () => Results.Ok()).RequireAuthorization("CanWriteOrders");Policies and resource-based checks
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanWriteOrders", p => p
.RequireAuthenticatedUser()
.RequireClaim("scope", "orders:write")
.RequireAssertion(ctx =>
ctx.User.HasClaim("tenant", ctx.Resource?.ToString() ?? "")));
options.AddPolicy("MinimumAge", p =>
p.Requirements.Add(new MinimumAgeRequirement(18)));
// A fallback policy protects everything that forgot to opt in
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
public sealed record MinimumAgeRequirement(int Age) : IAuthorizationRequirement;
public sealed class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
if (DateTime.TryParse(
context.User.FindFirst("dob")?.Value, out var dob) &&
dob.AddYears(requirement.Age) <= DateTime.UtcNow)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Resource-based authorisation: the check needs the loaded object
app.MapGet("/orders/{id:int}", async (
int id, IOrderRepo repo, IAuthorizationService auth, ClaimsPrincipal user) =>
{
var order = await repo.FindAsync(id);
if (order is null) return Results.NotFound();
var result = await auth.AuthorizeAsync(user, order, "CanReadOrder");
return result.Succeeded ? Results.Ok(order) : Results.Forbid();
});- Authorisation middleware must run after authentication, which means
UseAuthenticationbeforeUseAuthorization, and both afterUseRouting. - For a browser app with cookie authentication, every state-changing endpoint needs anti-forgery validation. Minimal APIs do this automatically when the anti-forgery middleware is added with
UseAntiforgery. - A fallback policy is the cheapest way to make security the default: an endpoint must opt out explicitly rather than opt in.
- Claims from a token are assertions by the issuer, not a substitute for an ownership query on your own data.
- Return 404 rather than 403 when revealing that the object exists is itself a disclosure.
- A resource-based handler is the right place for ownership, tenancy and state-machine rules, because the object is available there and nowhere else.
⚠️
A token that validates is not automatically a token with the right scope. Configure the audience and issuer, check the scopes the endpoint needs, and remember that a valid token issued for another service is a common way to reach an endpoint you did not intend to expose.
FAQ
Why is my endpoint returning 404 instead of 401?
Because the endpoint matched but authorisation failed and the challenge could not be issued, or a middleware order problem means authentication never ran. Check the middleware order first.
Should I store a JWT in a cookie or a header?
A header for APIs called by code, a cookie for a browser application. A cookie carries a CSRF risk and needs anti-forgery; a header needs somewhere safe to keep the token.
Related
Controllers, dependency injection and middleware Model validation and Problem Details responses
Last refreshed 2026-09-18.