Fine-grained, centralized access rules prevent the most damaging class of API bugs.
Authorization decides whether the authenticated user may perform an action. Apply it to endpoints with .RequireAuthorization() (minimal APIs), [Authorize] (controllers), or to whole route groups. .AllowAnonymous() / [AllowAnonymous] opt out.
Policies name a set of requirements and keep rules in one place:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("Admin", p => p.RequireRole("admin"))
.AddPolicy("CanWrite", p => p.RequireClaim("scope", "todos.write"));Use them with .RequireAuthorization("CanWrite"). Policy building blocks: RequireAuthenticatedUser, RequireRole, RequireClaim, RequireAssertion(ctx => ...) for inline logic, and custom IAuthorizationRequirement + AuthorizationHandler<T> classes for rules that need services (database lookups, time of day).
Set a fallback policy (SetFallbackPolicy(...RequireAuthenticatedUser()...)) to make every endpoint require login unless it explicitly allows anonymous access; this is safer than remembering to protect each new endpoint.
Resource-based authorization handles "may this user edit this document?". Load the resource, then call IAuthorizationService.AuthorizeAsync(user, document, "CanEdit") with a handler that compares e.g. the owner id to the user's id. Return Forbid() when it fails.
Test authorization rules with integration tests for both allowed and denied cases.
Example
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddCookie();
builder.Services.AddAuthorizationBuilder()
.AddPolicy("Admin", p => p.RequireRole("admin"))
.AddPolicy("CanWrite", p => p.RequireClaim("scope", "todos.write"))
.AddPolicy("Owner", p => p.RequireAssertion(ctx =>
ctx.Resource is Doc d && d.OwnerId == ctx.User.FindFirstValue(ClaimTypes.NameIdentifier)))
.SetFallbackPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
var app = builder.Build();
app.MapGet("/health", () => "ok").AllowAnonymous();
var admin = app.MapGroup("/admin").RequireAuthorization("Admin");
admin.MapDelete("/cache", () => Results.NoContent());
app.MapPost("/todos", () => Results.Created()).RequireAuthorization("CanWrite");
app.MapPut("/docs/{id}", async (string id, ClaimsPrincipal user, IAuthorizationService auth) =>
{
var doc = new Doc(id, OwnerId: "42"); // normally loaded from a database
var result = await auth.AuthorizeAsync(user, doc, "Owner");
return result.Succeeded ? Results.NoContent() : Results.Forbid();
});
app.Run();
record Doc(string Id, string OwnerId);Remember
- RequireAuthorization / [Authorize] apply policies to endpoints and groups
- Define named policies once with roles, claims or assertions
- A fallback policy protects endpoints by default
- Use IAuthorizationService for per-resource checks
Watch out for
- Checking roles with if statements scattered across handlers instead of named policies.