The resource doesn't handle authorization itself — it delegates to the agent's Person Server. The resource issues a resource token; the agent exchanges it at the PS for an auth token; then presents the auth token back. Requires sig=jwt signing mode.
sequenceDiagram
participant Agent
participant Resource
participant PS as Person Server
Agent->>Resource: GET /data (signed, sig=jwt with agent token)
Resource-->>Agent: 401 + resource token (aud=PS)
Agent->>PS: POST /token (signed, resource token in body)
PS-->>Agent: 200 + auth token (aa-auth+jwt)
Agent->>Resource: GET /data (signed, sig=jwt with auth token)
Resource-->>Agent: 200 OK
Automatic handling with AAuthClientBuilder:
using AAuth.Agent;
using AAuth.Crypto;
using AAuth;
// Hosted service: self-issue (no AP needed)
var key = AAuthKey.Generate();
using var client = AAuthClientBuilder.SelfIssuing(key)
.As("https://my-service.example", "aauth:my-service@my-service.example")
.WithKid("svc-key-1")
.WithPersonServer("https://ps.example")
.WithChallengeHandling()
.Build();
var response = await client.GetAsync("https://resource.example/data");
// ChallengeHandler intercepts the 401, exchanges the resource token,
// swaps to the auth token, and retries automatically.CLI/Desktop Agent (AP Enrollment)
For agents without a stable URL, enrol with an Agent Provider:
using AAuth.Agent;
using AAuth.Crypto;
using AAuth;
var keyStore = FileKeyStore.Default();
var localKeyHandle = configuration["AAuth:LocalKeyHandle"]!;
var key = await keyStore.LoadAsync(localKeyHandle)
?? throw new InvalidOperationException("Key not found. Run enrollment first.");
var apRefreshEndpoint = configuration["AAuth:ApRefreshEndpoint"]!;
using var client = new AAuthClientBuilder(key)
.WithTokenRefresh(AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build())
.WithChallengeHandling(personServer: "https://ps.example")
.Build();
var response = await client.GetAsync("https://resource.example/data");Manual Setup (Advanced)
This shows the internal handler pipeline for educational purposes. Use WithTokenRefresh + WithChallengeHandling in production code.
using AAuth.Agent;
using AAuth.Crypto;
using AAuth.Discovery;
using AAuth.HttpSig;
var keyStore = FileKeyStore.Default();
var key = await keyStore.LoadAsync(configuration["AAuth:LocalKeyHandle"]!);
var agentToken = "..."; // acquired via AP refresh endpoint
var tokenHolder = new AAuthTokenHolder(agentToken);
var signingHandler = new AAuthSigningHandler(key!,
new JwtSignatureKeyProvider(() => tokenHolder.Current))
{
InnerHandler = new HttpClientHandler()
};
var signedClient = new HttpClient(signingHandler);
var metadata = new MetadataClient(new HttpClient());
var exchange = new TokenExchangeClient(signedClient, metadata);
var challengeHandler = new ChallengeHandler(
exchange, tokenHolder, "https://ps.example")
{
InnerHandler = signingHandler
};
using var client = new HttpClient(challengeHandler);
var response = await client.GetAsync("https://resource.example/data");using AAuth.Agent;
using AAuth.Crypto;
var keyStore = FileKeyStore.Default();
var localKeyHandle = configuration["AAuth:LocalKeyHandle"]!;
var key = await keyStore.LoadAsync(localKeyHandle);
var apRefreshEndpoint = configuration["AAuth:ApRefreshEndpoint"]!;
builder.Services.AddAAuthAgent("ps-asserted", options =>
{
options.Key = key!;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});This registers a named HttpClient with signing + automatic challenge handling. Inject via IHttpClientFactory.CreateClient("ps-asserted"). The ChallengeHandler intercepts 401 responses, exchanges the resource token at the PS, and retries transparently.
See Dependency Injection for full options reference.
- Agent sends signed request with agent token → resource returns 401 + resource token
ChallengeHandlerintercepts: extracts resource token from responseTokenExchangeClient.ExchangeAsync()posts resource token to PS'stoken_endpoint- PS verifies the resource token (
TokenVerifier.VerifyResourceTokenAsync:typ/dwk/signature via the issuing resource's JWKS,exp/iat,aud,agent,agent_jkt), then verifies the agent identity, checks consent, and returns the auth token AAuthTokenHolderis updated with the auth tokenChallengeHandlerretries the original request (now with auth token in Signature-Key)
- Autonomous: PS has standing consent → returns auth token immediately (step 3→4)
- Deferred: PS requires user approval → returns 202 + pending URL → agent polls (see Deferred Consent)
| Status | Header/Token | Cause |
|---|---|---|
| 401 | Signature-Error: invalid_signature |
Signature doesn't verify at resource |
| 401 | Resource token with error claim |
Resource rejects agent identity |
| 403 | Auth token denied | PS issued auth token but resource policy still denies |
| 202 | Pending URL from PS | Deferred consent — user approval required |