AAuthChallengeMiddleware automatically issues 401 challenges with resource tokens when an agent presents only an agent token but the resource requires an auth token.
using AAuth;
using AAuth.Server.Challenge;
using AAuth.Server.Verification;
// Must be registered AFTER UseAAuthVerification
app.UseAAuthChallenge(new ChallengeOptions
{
AccessMode = AAuthAccessMode.RequireAuthToken,
});public enum AAuthAccessMode
{
// Accept any verified identity without requiring auth token
IdentityOnly,
// Require auth token — issue 401 challenge if only agent token present
RequireAuthToken,
}UseAAuthVerificationruns first and storesAAuthVerificationResultin features- If
AccessModeisRequireAuthTokenand the token is an agent token (not auth token):- Middleware mints a resource token (
aa-resource+jwt) scoped to the request - Returns
401 UnauthorizedwithAAuth-Requirement: requirement=auth-token; resource-token="<jwt>"
- Middleware mints a resource token (
- The agent's
ChallengeHandlercatches the 401, exchanges the resource token at its PS, and retries
public sealed class ChallengeOptions
{
// How to handle access decisions
public AAuthAccessMode AccessMode { get; init; } = AAuthAccessMode.RequireAuthToken;
// Resource signing key for minting resource tokens
public AAuthKey? ResourceSigningKey { get; init; }
// Key identifier for the resource signing key (kid in the resource token header)
public string? ResourceKeyId { get; init; }
// Resource identifier (used as iss in the resource token)
public string? ResourceIdentifier { get; init; }
// Explicit audience for resource tokens (e.g. the AS URL in a four-party flow).
// When null, audience is resolved from the agent token's ps claim (three-party).
public string? PersonServerAudience { get; init; }
// Default scopes to request in the resource token (space-separated)
public string? DefaultScopes { get; init; }
// Allowed Signature-Key schemes (null = allow all)
public IReadOnlySet<string>? AllowedSignatureKeySchemes { get; init; }
}app.UseAAuthVerification(new AAuthVerificationOptions
{
ResourceIdentifier = "https://resource.example",
RequireIssuerVerification = true,
});
app.UseAAuthChallenge(new ChallengeOptions
{
AccessMode = AAuthAccessMode.RequireAuthToken,
});
// Endpoints below here see only authorized requests
app.MapGet("/data", (HttpContext ctx) =>
{
var result = ctx.GetAAuthVerification()!;
// result.Level == AAuthLevel.Authorized
});DefaultScopes controls which scope the minted resource token requests. To protect
different endpoints with different scopes, give each one its own challenge branch so
the 401 asks for exactly the scope that endpoint enforces. This is the pattern the
WhoAmI sample uses: /jwt challenges for whoami, while the step-up /jwt/admin
endpoint challenges for whoami:admin.
ChallengeOptions ChallengeForScope(string scope) => new()
{
AccessMode = AAuthAccessMode.RequireAuthToken,
ResourceSigningKey = resourceKey,
ResourceKeyId = "whoami-1",
ResourceIdentifier = resourceUrl,
DefaultScopes = scope,
};
// /jwt/admin — declared first so the more specific segment wins. Challenges for
// the elevated scope.
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/jwt/admin"),
branch =>
{
branch.UseAAuthVerification(fullVerification);
branch.UseAAuthChallenge(ChallengeForScope("whoami:admin"));
});
// /jwt — three-party baseline. Challenges for the base scope.
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/jwt")
&& !ctx.Request.Path.StartsWithSegments("/jwt/admin"),
branch =>
{
branch.UseAAuthVerification(fullVerification);
branch.UseAAuthChallenge(ChallengeForScope("whoami"));
});Because each branch is an isolated pipeline, an agent that lacks the required scope
receives a challenge for that endpoint's scope and re-exchanges at its PS for an
auth token carrying it. See samples/WhoAmI for the full set of branches.