Register AAuth services in ASP.NET Core and hosted applications using the built-in DI extensions.
How your agent obtains its token depends on its deployment model:
- Hosted services (web apps, APIs, orchestrators with a stable URL): Self-issue agent tokens at runtime. Generate a key at startup, publish
/.well-known/aauth-agent.json, and build tokens locally. No external AP needed. - CLI / desktop / mobile agents (no stable URL): Enrol with an Agent Provider once (provisioning step), then refresh tokens from the AP at runtime.
In both cases, the agent token is short-lived (typically 1 hour) and refreshed automatically by the SDK. You never persist it.
flowchart LR
subgraph Hosted
H1["Startup: Generate key"] --> H2["Publish /.well-known/aauth-agent.json"]
H2 --> H3["Runtime: self-issue token via AgentTokenBuilder"]
end
subgraph CLI/Desktop
P["Provisioning: EnrolAsync(keyStore)"] --> C["App config: AAuth:LocalKeyHandle"]
C --> S["Startup: keyStore.LoadAsync → AddAAuthAgent"]
S --> R["Runtime: SDK calls AP refresh before expiry"]
end
See Bootstrap & Enrollment for the CLI/desktop provisioning step, or Getting Started for the self-issued path.
No enrollment required. Generate or load a key and register:
var key = AAuthKey.Generate(); // or load from persistent storage
builder.Services.AddAAuthAgent("signing-only", options =>
{
options.Key = key;
options.UseHwk();
});No AP enrollment needed. The service generates a key and self-issues tokens:
var key = AAuthKey.Generate();
const string Kid = "svc-key-1";
var issuer = "https://my-service.example";
builder.Services.AddAAuthAgent("self-issued", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = SelfIssuedTokenRefresher.Create(key, issuer, "aauth:my-service@my-service.example")
.WithKid(Kid)
.WithPersonServer("https://ps.example")
.Build();
});
// Also publish agent metadata so verifiers can discover the JWKS
app.MapAAuthAgentWellKnown(new AAuthAgentMetadataOptions
{
Issuer = issuer,
SigningKeys = new Dictionary<string, AAuthKey> { [Kid] = key },
});Load the key by local handle from the store and configure token refresh:
var keyStore = FileKeyStore.Default();
var localKeyHandle = configuration["AAuth:LocalKeyHandle"]!;
var key = await keyStore.LoadAsync(localKeyHandle)
?? throw new InvalidOperationException($"Key '{localKeyHandle}' not found.");
var apRefreshEndpoint = configuration["AAuth:ApRefreshEndpoint"]!;
builder.Services.AddAAuthAgent("identity", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});If your AP issues longer-lived tokens and you manage refresh externally, you can still configure the refresher accordingly:
builder.Services.AddAAuthAgent("identity", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});When the Person Server requires user approval, provide interaction callbacks:
builder.Services.AddAAuthAgent("interactive", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
options.InteractionHandling = true;
options.InteractionHandlingOptions = io =>
{
io.OnInteractionRequired = async (url, code, ct) =>
{
// Present URL and code to user
logger.LogInformation("Approve at {Url} with code {Code}", url, code);
};
io.PollingTimeout = TimeSpan.FromMinutes(3);
};
});For long-lived agents, enable automatic token refresh before expiry:
builder.Services.AddAAuthAgent("refreshing", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create("https://ap.example/refresh", localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});builder.Services.AddAAuthResource(options =>
{
options.Issuer = "https://my-resource.example";
options.SigningKeys = new() { ["key-1"] = resourceKey };
});
var app = builder.Build();
app.UseAAuthVerification(); // HTTP sig + JWT issuer verification middleware
app.MapAAuthWellKnown(); // /.well-known/aauth-resource.json + /jwks.jsonbuilder.Services.AddAAuthResource(options =>
{
options.Issuer = "https://my-resource.example";
options.SigningKeys = new() { ["key-1"] = resourceKey };
options.ClientName = "Supply Chain Service";
options.ScopeDescriptions = new()
{
["data:read"] = "Read supply chain data",
["data:write"] = "Modify supply chain records",
};
});Override the authorization endpoint for advanced scenarios (e.g., custom access server):
builder.Services.AddAAuthResource(options =>
{
options.Issuer = "https://my-resource.example";
options.SigningKeys = new() { ["key-1"] = resourceKey };
options.AuthorizationEndpoint = "https://as.example/authorize";
});To map verification results into a ClaimsPrincipal and enforce per-endpoint
access, register the AAuth authentication scheme, the authorization handlers, and
any named scope/role policies:
builder.Services.AddAAuthAuthentication(); // maps result → ClaimsPrincipal
builder.Services.AddAAuthAuthorization(); // scope handler + built-in policies
// Named convenience policies (apply with RequireAuthorization(...)):
builder.Services.AddAAuthScopePolicy("AAuth.Scope.data:read", "data:read");
builder.Services.AddAAuthRolePolicy("AAuth.Role.admin", "admin");AddAAuthAuthorization()registers the built-inAAuth.Authenticated,AAuth.Identified, andAAuth.Authorizedpolicies plusAAuthScopeHandler.AddAAuthScopePolicy(policyName, requiredScope)registers a policy that requires anAAuthLevel.Authorizedauth token carryingrequiredScope— an agent-token-only (PoP) request cannot satisfy it.AddAAuthRolePolicy(policyName, requiredRole)registers a policy that requires anAAuthLevel.Authorizedauth token plusrequiredRole(mapped from the token'srolesclaim to the standardClaimTypes.Role).
See Authorization Policies for details.
Register shared MetadataClient and JwksClient singletons with custom cache settings:
builder.Services.AddAAuthDiscovery(options =>
{
options.MetadataCacheTtl = TimeSpan.FromMinutes(10);
options.JwksCacheTtl = TimeSpan.FromHours(2);
});Both AddAAuthAgent and AddAAuthResource register their own discovery clients if AddAAuthDiscovery has not been called. Call it explicitly to share instances and control cache behavior.
public class MyAgentService(IHttpClientFactory factory)
{
private readonly HttpClient _client = factory.CreateClient("identity");
public async Task<string> FetchDataAsync()
{
var response = await _client.GetAsync("https://resource.example/data");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}Register different clients for different resources or signing modes:
builder.Services.AddAAuthAgent("internal-api", options =>
{
options.Key = key;
options.PersonServer = "https://ps.internal";
options.TokenRefresher = internalRefresher;
});
builder.Services.AddAAuthAgent("external-api", options =>
{
options.Key = externalKey;
options.PersonServer = "https://ps.partner.example";
options.TokenRefresher = externalRefresher;
});An app that verifies inbound AAuth requests AND makes signed outbound requests.
See samples/Orchestrator for a full working implementation with call chaining.
var builder = WebApplication.CreateBuilder(args);
// Inbound: verify signatures on incoming requests
builder.Services.AddAAuthResource(options =>
{
options.Issuer = "https://my-service.example";
options.SigningKeys = new() { ["rs-1"] = resourceKey };
});
// Outbound: sign requests to downstream resources
builder.Services.AddAAuthAgent("downstream", options =>
{
options.Key = agentKey;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});
var app = builder.Build();
app.UseAAuthVerification();
app.MapAAuthWellKnown();
app.MapGet("/data", async (HttpContext ctx, IHttpClientFactory factory) =>
{
// Inbound request was verified by middleware
var parsed = ctx.GetAAuthParsedKey()!;
// Make signed outbound request
var client = factory.CreateClient("downstream");
var downstream = await client.GetStringAsync("https://other-resource.example/api");
return Results.Ok(new { agent = parsed.Payload?["sub"]?.ToString(), downstream });
});
app.Run();| Property | Type | Default | Description |
|---|---|---|---|
Key |
IAAuthKey |
required | Agent signing key (must have private component) |
BaseAddress |
Uri? |
null |
Target resource URL |
SignatureKeyProvider |
ISignatureKeyProvider? |
null |
Custom signature key provider |
PersonServer |
string? |
null |
PS URL; with ChallengeHandling, enables challenge flow |
ChallengeHandling |
bool |
false |
Enable challenge handling |
ChallengeHandlingOptions |
Action<ChallengeHandlingOptions>? |
null |
Configure challenge handling behavior |
InteractionHandling |
bool |
false |
Enable interaction handling |
InteractionHandlingOptions |
Action<InteractionHandlingOptions>? |
null |
Configure interaction handling behavior |
TokenRefresher |
ITokenRefresher? |
null |
Auto-refresh before token expiry |
RefreshThreshold |
TimeSpan? |
null |
Time before expiry to trigger refresh |
Capabilities |
string[]? |
null |
Agent capabilities to advertise |
InnerHandler |
HttpMessageHandler? |
null |
Custom inner HTTP handler |
CallChainProvider |
Func<string?>? |
null |
Provider for upstream auth token (call chaining) |
| Property | Type | Default | Description |
|---|---|---|---|
Issuer |
string |
required | Resource HTTPS URL (metadata + audience) |
SigningKeys |
Dictionary<string, AAuthKey> |
empty | Keys for signing resource tokens |
ClientName |
string? |
null |
Human-readable name in metadata |
ScopeDescriptions |
Dictionary<string, string>? |
null |
Scope descriptions in metadata |
SignatureWindow |
int? |
null |
Advertised signature validity (seconds) |
AuthorizationEndpoint |
string? |
null |
AS authorization URL |
RevocationEndpoint |
string? |
null |
Revocation endpoint URL |
| Property | Type | Default | Description |
|---|---|---|---|
MetadataCacheTtl |
TimeSpan |
5 min | How long to cache well-known metadata |
JwksCacheTtl |
TimeSpan |
1 hour | How long to cache JWKS documents |
For intermediary services that act as both resource and agent, AAuthClientBuilder provides call-chaining methods:
// From HttpContext (reads UpstreamAuthTokenFeature set by middleware)
var client = new AAuthClientBuilder(key)
.UseJwt(() => tokenHolder.Token)
.WithTokenRefresh(refresher)
.WithCallChaining(httpContext)
.Build();
// From a raw upstream token string
var client = new AAuthClientBuilder(key)
.UseJwt(() => tokenHolder.Token)
.WithTokenRefresh(refresher)
.WithCallChaining(upstreamAuthToken)
.Build();
// From a dynamic provider
var client = new AAuthClientBuilder(key)
.UseJwt(() => tokenHolder.Token)
.WithTokenRefresh(refresher)
.WithCallChaining(() => GetUpstreamToken())
.Build();WithCallChaining automatically:
- Routes downstream exchanges to the correct PS/AS via
CallChainingRouter - Passes
upstream_tokenin exchange POST body - Inserts
MissionForwardingHandlerto propagateAAuth-Missionheaders - Handles the full 401 → exchange → retry cycle