How the AAuth SDK turns a verified request into an ASP.NET Core ClaimsPrincipal
and enforces access, plus how to wire it up in both hosting styles — minimal APIs
and classic MVC controllers.
This page ties together two adjacent topics:
- Verification Middleware — proof-of-possession and token verification (the authN input).
- Authorization Policies — scope, role, and level policies (the authZ rules).
AAuth runs as three ordered layers. The order matters: each layer consumes what the previous one produced.
- Verification (
UseAAuthVerification) — verifies the HTTP signature (RFC 9421) and, for three-party flows, the auth token against the issuer's JWKS. It writes anAAuthVerificationResulttoHttpContext.Features. - Challenge (
UseAAuthChallenge, three-party endpoints only) — when only an agent token is presented, returns a401with a resource token requesting the endpoint's scope. - Authentication + authorization (
UseAuthentication/UseAuthorization) —AAuthAuthenticationHandlermaps the verification result to aClaimsPrincipal; scope/role/level policies then decide access per endpoint.
flowchart LR
req([request]) --> ver["UseAAuthVerification<br/>(writes Features)"]
ver --> chal["UseAAuthChallenge<br/>(401 + resource token)"]
chal --> authn["UseAuthentication<br/>(Features → Principal)"]
authn --> authz["UseAuthorization<br/>(policy check)"]
authz --> ep([endpoint])
Well-known endpoints come first. Map
MapAAuthResourceWellKnown(...)beforeUseAAuthVerificationso the metadata document and JWKS stay reachable without an AAuth signature.
AAuthAuthenticationHandler is the bridge from verification to identity. It reads
AAuthVerificationResult from HttpContext.Features and produces a
ClaimsPrincipal. Register it with AddAAuthAuthentication().
The verification level records how strongly the caller is identified:
public enum AAuthLevel
{
Pseudonymous, // hwk scheme — key-only identity
Identified, // jwt / jwks_uri — agent identity known
Authorized, // aa-auth+jwt — full PS/AS authorization
}- Pseudonymous — the request proved possession of a key (
hwk/jkt-jwt) but carries no agent identity. - Identified — the agent's identity is verified (
jwt/jwks_uri), but no PS has authorized access. - Authorized — a verified
aa-auth+jwtis present; the PS/AS has authorized the agent for the asserted scope.
The handler maps the verification result to claims (see
Authorization Policies for
the full table). The identity claims asserted by a Person Server — sub
(NameIdentifier), each role, and each aauth:group — carry
Claim.Issuer == iss, the asserting PS. The canonical user key is therefore the
(iss, sub) pair, surfaced as the composite aauth:sub_iss claim
(AAuthAuthenticationHandler.SubjectIssuerClaimType).
subalone is not an identity. The samesubasserted by two different Person Servers is two different users. Key your application records on(iss, sub)(or theaauth:sub_issclaim), never onsubalone. Issuer trust is fail-closed: only auth tokens whoseissis inAAuthVerificationOptions.TrustedAuthTokenIssuersare honored.
Register the handlers and built-in policies with AddAAuthAuthorization(), then
add named policies:
builder.Services.AddAAuthAuthentication();
builder.Services.AddAAuthAuthorization();
builder.Services.AddAAuthScopePolicy("AAuth.Scope.data:read", "data:read");
builder.Services.AddAAuthRolePolicy("AAuth.Role.admin", "admin");AddAAuthAuthorization() registers the built-in level policies
AAuth.Authenticated, AAuth.Identified, and AAuth.Authorized.
Scope and role policies both require AAuthLevel.Authorized — a signature-only or
agent-token-only request can never satisfy them, even if it carried a matching
scope claim. See Authorization Policies for the scope
handler semantics and the role/group discussion.
This is what the WhoAmI sample uses. Per-mode
verification branches with UseWhen, then per-endpoint policies with
RequireAuthorization.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(new AAuthVerifier());
builder.Services.AddSingleton<IJtiStore, InMemoryJtiStore>();
builder.Services.AddAAuthAuthentication();
builder.Services.AddAAuthAuthorization();
builder.Services.AddAAuthScopePolicy("AAuth.Scope.whoami", "whoami");
builder.Services.AddAAuthScopePolicy("AAuth.Scope.whoami:admin", "whoami:admin");
builder.Services.AddAAuthRolePolicy("AAuth.Role.whoami-admin", "whoami-admin");
var app = builder.Build();
// Reachable without a signature.
app.MapAAuthResourceWellKnown(metadataOptions);
// Three-party branch: verify the auth token, challenge for the scope.
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/jwt"),
branch =>
{
branch.UseAAuthVerification(new AAuthVerificationOptions
{
ResourceIdentifier = resourceUrl,
RequireIssuerVerification = true,
TrustedAuthTokenIssuers = trustedPersonServers,
});
branch.UseAAuthChallenge(challengeOptions);
});
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/jwt", (HttpContext ctx) => Results.Ok(/* ... */))
.RequireAuthorization("AAuth.Scope.whoami");
app.MapGet("/jwt/roles", (HttpContext ctx) => Results.Ok(/* ... */))
.RequireAuthorization("AAuth.Role.whoami-admin");
app.Run();MapGroup works the same way when several endpoints share a policy:
var admin = app.MapGroup("/admin").RequireAuthorization("AAuth.Scope.whoami:admin");
admin.MapGet("/profile", () => Results.Ok(/* ... */));
admin.MapPost("/profile", () => Results.Ok(/* ... */));Service registration is identical. The difference is only how endpoints declare
their policies — via [Authorize] attributes instead of RequireAuthorization.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSingleton(new AAuthVerifier());
builder.Services.AddSingleton<IJtiStore, InMemoryJtiStore>();
builder.Services.AddAAuthAuthentication();
builder.Services.AddAAuthAuthorization();
builder.Services.AddAAuthScopePolicy("AAuth.Scope.data:read", "data:read");
builder.Services.AddAAuthRolePolicy("AAuth.Role.admin", "admin");
var app = builder.Build();
app.MapAAuthResourceWellKnown(metadataOptions);
app.UseAAuthVerification(new AAuthVerificationOptions
{
ResourceIdentifier = resourceUrl,
RequireIssuerVerification = true,
TrustedAuthTokenIssuers = trustedPersonServers,
});
app.UseAAuthChallenge(challengeOptions);
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();[ApiController]
[Route("data")]
public sealed class DataController : ControllerBase
{
// Scope policy: requires Authorized level + the `data:read` scope.
[HttpGet]
[Authorize("AAuth.Scope.data:read")]
public IActionResult Get() => Ok(/* ... */);
// Role policy: requires Authorized level + the `admin` role.
[HttpDelete("{id}")]
[Authorize("AAuth.Role.admin")]
public IActionResult Delete(string id) => NoContent();
// Roles also work with the framework's RequireRole / Roles syntax,
// because AAuth maps the `roles` claim to ClaimTypes.Role.
[HttpPost]
[Authorize(Roles = "admin")]
public IActionResult Create() => Created(string.Empty, null);
}Roles are PS-namespaced here too.
[Authorize(Roles = "admin")]matches a role asserted by any trusted PS. If you need to bind a role to a specific issuer, enforce it in a custom policy that inspectsClaim.Issueror the(iss, sub)pair fromAAuthVerificationResult.
Both styles expose the same data. From the ClaimsPrincipal:
var subIss = User.FindFirst(AAuthAuthenticationHandler.SubjectIssuerClaimType)?.Value; // "iss|sub"
var issuer = User.FindFirst(ClaimTypes.NameIdentifier)?.Issuer; // asserting PSOr directly from the verification feature:
var result = HttpContext.Features.Get<AAuthVerificationResult>();
// result.Level, result.Subject, result.Issuer, result.Scopes, result.Roles, result.Groups- Verification Middleware — PoP + token verification
- Authorization Policies — scope/role/level policies
- Challenge Middleware — emitting 401 resource-token challenges
- Token Issuance — building and verifying tokens