Policies, Roles and Claims
[Authorize] with roles, named policies, custom requirements and handlers, and resource checks for 'only the owner may edit this'.
builder.Services.AddAuthorizationBuilder().AddPolicy("Name", policy => policy.RequireClaim("type", "value"))Three ways to say who may do what
Authorisation runs after authentication, against a user who is already known. The simplest form is the [Authorize] attribute, or RequireAuthorization() on an endpoint, which are the same rule written two ways. No credential gives 401. A credential that fails the rule gives 403.
[Authorize(Roles = "admin")] looks for a role claim. A role is only a claim of type ClaimTypes.Role, which is what IsInRole reads. Roles are coarse: "admin" says nothing about which actions an admin may take, and the list grows with every exception.
Policies
A policy is a named rule, registered once and referred to by name everywhere. It is also the longhand of a role check. These are the same rule:
[Authorize(Roles = "admin")]
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminsOnly", policy => policy.RequireRole("admin"));
// then [Authorize(Policy = "AdminsOnly")]
For a rule no built-in Require method expresses, write a requirement and a handler. The requirement carries the data. The handler calls context.Succeed(requirement) when satisfied and simply returns when not.
Owner checks need the thing itself
"Only the owner may edit this document" cannot be decided from claims alone, because the owner is a property of the document. Load it, then ask IAuthorizationService.AuthorizeAsync(user, document, requirement) and return Results.Forbid() when the result is not Succeeded.
What trips people up
Not calling Succeed denies. Calling Fail is stronger: it vetoes the requirement even if another registered handler would have satisfied it. Against the program below, /reports with no token returns 401, and with a valid token lacking the payroll claim returns 403.
Example
using Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// The handler reads the signing keys and the issuer name from the
// authority's discovery document, and validates both.
options.Authority = "https://login.example.com";
options.Audience = "orders-api";
});
// Two named rules. AddAuthorizationBuilder was added in .NET 7.
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminsOnly", policy => policy.RequireRole("admin"))
.AddPolicy("PayrollOnly", policy => policy
.RequireAuthenticatedUser()
.RequireClaim("department", "payroll"));
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// RequireAuthorization("X") on an endpoint is the same rule as
// [Authorize(Policy = "X")] on a controller action.
app.MapGet("/reports", () => "payroll report").RequireAuthorization("PayrollOnly");
app.MapGet("/users", () => "user list").RequireAuthorization("AdminsOnly");
app.Run();When to use it
- Restrict an administrative endpoint to users holding a particular role.
- Replace a growing list of role names with named policies that describe the actual rule.
- Allow a user to edit only the records they own, without repeating the check in each endpoint.
- Work out why a signed-in user gets 403 while an anonymous caller gets 401 on the same route.
More examples
A requirement and its handler
Where to go once RequireClaim and RequireRole run out: a rule with its own data, or one that needs to read a service. AuthorizationHandlerContext has no HttpContext property, so a handler that needs a scoped service either registers with AddScoped instead of AddSingleton, or takes IHttpContextAccessor in its constructor and resolves the service from HttpContext.RequestServices.
using Microsoft.AspNetCore.Authorization;
namespace Authz;
// A requirement is a marker that carries whatever the check needs. The
// parameter list is a primary constructor; the longhand is a private
// field plus a constructor that assigns it.
public sealed class DepartmentRequirement(string department) : IAuthorizationRequirement
{
public string Department { get; } = department;
}
// A handler decides whether one requirement is met.
public sealed class DepartmentHandler : AuthorizationHandler<DepartmentRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DepartmentRequirement requirement)
{
string? department = context.User.FindFirst("department")?.Value;
if (string.Equals(department, requirement.Department, StringComparison.Ordinal))
{
context.Succeed(requirement);
}
// Returning without calling Succeed is enough to deny. Call Fail()
// only to veto a requirement another handler might satisfy.
return Task.CompletedTask;
}
}
// Program.cs (excerpt)
// builder.Services.AddSingleton<IAuthorizationHandler, DepartmentHandler>();
// builder.Services.AddAuthorizationBuilder()
// .AddPolicy("PayrollOnly", policy => policy
// .RequireAuthenticatedUser()
// .AddRequirements(new DepartmentRequirement("payroll")));Only the owner may edit this
OperationAuthorizationRequirement is a built-in requirement whose only content is a Name, which saves declaring one type per verb. This handler cannot run before the document is loaded, so the check belongs in the endpoint rather than in the middleware.
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Authz;
public sealed class Document
{
public required string Id { get; init; }
public required string OwnerId { get; init; }
}
// The two-parameter base class adds the resource being checked.
public sealed class DocumentOwnerHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Document resource)
{
string? userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (requirement.Name == "Edit" && userId == resource.OwnerId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}Calling the resource check from an endpoint
The pattern for every per-row permission check. RequireAuthorization() still guards the endpoint so an anonymous caller gets 401 without a database read, and the ownership decision stays in the handler, where one change covers every endpoint that edits a document.
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
// Program.cs (excerpt)
app.MapPut("/documents/{id}", async (string id, DocumentStore store,
IAuthorizationService authorization, ClaimsPrincipal user) =>
{
Document? document = await store.FindAsync(id);
if (document is null)
{
return Results.NotFound();
}
AuthorizationResult result = await authorization.AuthorizeAsync(
user, document, new OperationAuthorizationRequirement { Name = "Edit" });
if (!result.Succeeded)
{
// 403, not 404: the caller is known, and not permitted.
return Results.Forbid();
}
await store.SaveAsync(document);
return Results.NoContent();
}).RequireAuthorization();
Discussion