From b72bbbbfbf055ce6d4baa2b69489e1239ebf4ee8 Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Tue, 26 May 2026 20:06:58 +0000 Subject: [PATCH 1/3] docs: clarify self-issuance vs AP enrollment across all docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update documentation to reflect that hosted services (web apps, APIs, orchestrators) act as their own AP per spec §Self-Hosted Agents and self-issue agent tokens. AP enrollment is only needed for CLI tools, desktop apps, and mobile agents without a stable URL. - getting-started.md: add Self-Issued section before AP Bootstrap - agent-token-jwt.md: show both self-issued and AP-enrolled examples - overview.md: fix Requires column (JWKS host or self-hosted) - bootstrap-enrollment.md: scope to CLI/desktop agents - ps-asserted-access.md: lead with self-issued, AP in collapsible - concepts.md: agent token issued by 'Agent Provider or Self' - Orchestrator/README.md: remove dead AP config, update details --- docs/concepts.md | 2 +- docs/getting-started.md | 38 ++++++++++++++++++++++++-- docs/signing-modes/agent-token-jwt.md | 32 ++++++++++++++++++++-- docs/signing-modes/overview.md | 4 +-- docs/workflows/bootstrap-enrollment.md | 2 +- docs/workflows/ps-asserted-access.md | 35 ++++++++++++++++++++++-- samples/Orchestrator/README.md | 7 ++--- 7 files changed, 105 insertions(+), 15 deletions(-) diff --git a/docs/concepts.md b/docs/concepts.md index 64df815..1350ee6 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -50,7 +50,7 @@ See [Missions](https://explorer.aauth.dev/missions/compare). | Token | Type Header | Issued By | Purpose | SDK | |-------|-------------|-----------|---------|-----| -| Agent Token | `aa-agent+jwt` | Agent Provider | Binds key → identity | `AgentTokenBuilder` | +| Agent Token | `aa-agent+jwt` | Agent Provider or Self | Binds key → identity | `AgentTokenBuilder` | | Resource Token | `aa-resource+jwt` | Resource | Challenge: "get auth from my PS/AS" | `ResourceTokenBuilder` | | Auth Token | `aa-auth+jwt` | PS or AS | Proves user authorized this agent | `AuthTokenBuilder` | diff --git a/docs/getting-started.md b/docs/getting-started.md index 16c954c..1fec976 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -75,9 +75,43 @@ public class MyService(IHttpClientFactory factory) - `AAuthSigningHandler` signs the request per RFC 9421 covering `@method`, `@authority`, `@path`, and `signature-key`. - The resource verifies the signature using the inline public key from `Signature-Key`. -## Bootstrap with an Agent Provider (Three-Party Flow) +## Self-Issued Agent Tokens (Hosted Services) -For production scenarios, agents register with an **Agent Provider (AP)** to get an identity-bound agent token. Enrollment is a **provisioning step** that runs once (in a CLI tool or setup script). The durable signing key is generated inside a keystore and never extracted — the app references it by ID. The agent token is short-lived (typically 1 hour) and refreshed automatically by the SDK. +Hosted services (web apps, APIs, orchestrators) that have a stable URL act as their own Agent Provider per spec §Self-Hosted Agents. They generate a key at startup, publish agent metadata at `/.well-known/aauth-agent.json`, and self-sign agent tokens. No external AP enrollment is needed. + +```csharp +using AAuth.Crypto; +using AAuth.HttpSig; +using AAuth.Server; +using AAuth.Tokens; + +var key = AAuthKey.Generate(); +const string Kid = "my-service-1"; +var issuer = "https://my-service.example"; + +// Publish agent metadata so verifiers can discover the JWKS +app.MapAAuthAgentWellKnown(new AAuthAgentMetadataOptions +{ + Issuer = issuer, + SigningKeys = new Dictionary { [Kid] = key }, +}); + +// Self-issue agent tokens for outbound requests +using var client = new AAuthClientBuilder(key) + .WithTokenRefresh(async (ctx, ct) => new AgentTokenBuilder + { + Issuer = issuer, + Subject = "aauth:my-service@my-service.example", + KeyId = Kid, + Key = key, + }.Build()) + .WithChallengeHandling("https://ps.example") + .Build(); +``` + +## Bootstrap with an Agent Provider (CLI / Desktop Agents) + +For agents that do NOT have a stable URL (CLI tools, desktop apps, mobile apps), registration with an external **Agent Provider (AP)** provides identity and key discovery. Enrollment is a **provisioning step** that runs once (in a CLI tool or setup script). The durable signing key is generated inside a keystore and never extracted — the app references it by ID. The agent token is short-lived (typically 1 hour) and refreshed automatically by the SDK. ### Provisioning (run once per device/install) diff --git a/docs/signing-modes/agent-token-jwt.md b/docs/signing-modes/agent-token-jwt.md index b7fcb0d..7f2b5c3 100644 --- a/docs/signing-modes/agent-token-jwt.md +++ b/docs/signing-modes/agent-token-jwt.md @@ -10,10 +10,36 @@ The agent presents its full agent token inline. The resource (or Person Server) - When the resource needs to discover the agent's Person Server (from the `ps` claim) - When the resource needs verified agent identity with issuer attestation -**Prerequisite:** Agent must have enrolled with an Agent Provider to obtain an `aa-agent+jwt`. +**Prerequisite:** Agent must have an `aa-agent+jwt` — either self-issued (hosted services that publish their own JWKS) or obtained from an Agent Provider (CLI/desktop agents that enrol). ## Code Example +**Hosted service (self-issued):** + +```csharp +using AAuth.Crypto; +using AAuth.HttpSig; +using AAuth.Tokens; + +var key = AAuthKey.Generate(); + +using var client = new AAuthClientBuilder(key) + .WithTokenRefresh(async (ctx, ct) => new AgentTokenBuilder + { + Issuer = "https://my-service.example", + Subject = "aauth:my-service@my-service.example", + KeyId = "svc-key-1", + Key = key, + PersonServer = "https://ps.example", + }.Build()) + .WithChallengeHandling("https://ps.example") + .Build(); + +var response = await client.GetAsync("https://resource.example/data"); +``` + +**CLI/Desktop agent (AP-enrolled):** + ```csharp using AAuth.Agent; using AAuth.HttpSig; @@ -51,8 +77,8 @@ using var client = new HttpClient(handler); ## What the Resource Sees - `Signature-Key: sig=jwt;jwt="eyJhbGciOi..."` -- Resource decodes the JWT: finds `iss` (AP), `sub` (agent ID), `cnf.jwk` (bound key), optionally `ps` (Person Server URL) -- Resource verifies: JWT signature (against AP's JWKS) + request signature (against `cnf.jwk`) +- Resource decodes the JWT: finds `iss` (agent's own URL or AP), `sub` (agent ID), `cnf.jwk` (bound key), optionally `ps` (Person Server URL) +- Resource verifies: JWT signature (against issuer's JWKS via `{iss}/.well-known/aauth-agent.json`) + request signature (against `cnf.jwk`) ## Verification diff --git a/docs/signing-modes/overview.md b/docs/signing-modes/overview.md index caad528..e7788a8 100644 --- a/docs/signing-modes/overview.md +++ b/docs/signing-modes/overview.md @@ -17,8 +17,8 @@ All AAuth signing modes use HTTP Message Signatures (RFC 9421). The difference i |------|----------|----------| | Anonymous | Public endpoints, no access control | Nothing | | Pseudonymous (`hwk`) | Accountable access, rate-limiting by key | Just a keypair | -| Agent Identity (`jwks_uri`) | Access control by identity, replacing API keys | Agent Provider (publishes per-agent JWKS) | -| Agent Token (`jwt`) | Full PS-AS authorization flows | Agent Provider + Person Server | +| Agent Identity (`jwks_uri`) | Access control by identity, replacing API keys | JWKS host (AP or self-hosted) | +| Agent Token (`jwt`) | Full PS-AS authorization flows | Token issuer (AP or self-issued) + Person Server | ## SDK Types diff --git a/docs/workflows/bootstrap-enrollment.md b/docs/workflows/bootstrap-enrollment.md index 026a237..8209bb7 100644 --- a/docs/workflows/bootstrap-enrollment.md +++ b/docs/workflows/bootstrap-enrollment.md @@ -2,7 +2,7 @@ > [Signature-Key Schemes](https://explorer.aauth.dev/foundations/schemes) -Overview: Before an agent can use `jwks_uri` or `jwt` signing modes, it must register with an Agent Provider (AP) to get an agent token. This is the bootstrap step. For `hwk` (pseudonymous), no bootstrap is needed. +Overview: CLI tools, desktop apps, and mobile agents that lack a stable URL register with an Agent Provider (AP) to get an agent token. This is the bootstrap step. Hosted services with a stable URL can instead self-issue tokens (see [Getting Started](../getting-started.md#self-issued-agent-tokens-hosted-services)). For `hwk` (pseudonymous), no bootstrap is needed. ## Prerequisites diff --git a/docs/workflows/ps-asserted-access.md b/docs/workflows/ps-asserted-access.md index 4cb926a..e7275f7 100644 --- a/docs/workflows/ps-asserted-access.md +++ b/docs/workflows/ps-asserted-access.md @@ -25,6 +25,37 @@ sequenceDiagram Automatic handling with `AAuthClientBuilder`: +```csharp +using AAuth.Crypto; +using AAuth.HttpSig; +using AAuth.Tokens; + +// Hosted service: self-issue (no AP needed) +var key = AAuthKey.Generate(); +var issuer = "https://my-service.example"; + +using var client = new AAuthClientBuilder(key) + .WithTokenRefresh(async (ctx, ct) => new AgentTokenBuilder + { + Issuer = issuer, + Subject = "aauth:my-service@my-service.example", + KeyId = "svc-key-1", + Key = key, + PersonServer = "https://ps.example", + }.Build()) + .WithChallengeHandling(personServer: "https://ps.example") + .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: + ```csharp using AAuth.Agent; using AAuth.HttpSig; @@ -44,10 +75,10 @@ using var client = new AAuthClientBuilder(key) .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. ``` +
+
Manual Setup (Advanced) diff --git a/samples/Orchestrator/README.md b/samples/Orchestrator/README.md index 349abfb..de4ab0d 100644 --- a/samples/Orchestrator/README.md +++ b/samples/Orchestrator/README.md @@ -54,9 +54,8 @@ dotnet run --project samples/Orchestrator |-----|---------|---------| | `AAuth:Issuer` | `http://localhost:5200` | Orchestrator's resource identifier | | `AAuth:Downstream` | `http://localhost:5000` | Downstream resource (WhoAmI) URL | -| `AAuth:AgentProvider` | `http://localhost:5301` | AP for enrollment | | `AAuth:PersonServer` | `http://localhost:5100` | PS for token exchange | -| `AAuth:AgentId` | `aauth:orchestrator@ap.example` | Orchestrator's agent identity | +| `AAuth:AgentId` | `aauth:orchestrator@localhost:5200` | Orchestrator's agent identity | ## Using with AgentConsole @@ -68,7 +67,7 @@ curl -X POST http://localhost:5100/admin/consent \ curl -X POST http://localhost:5100/admin/consent \ -H "Content-Type: application/json" \ - -d '{"agent":"aauth:orchestrator@ap.example","resource":"http://localhost:5000"}' + -d '{"agent":"aauth:orchestrator@localhost:5200","resource":"http://localhost:5000"}' # Call through the chain dotnet run --project samples/AgentConsole -- http://localhost:5200 \ @@ -77,6 +76,6 @@ dotnet run --project samples/AgentConsole -- http://localhost:5200 \ ## Key Implementation Details -1. **Lazy enrollment**: The Orchestrator enrolls with the AP on first request (not at startup). +1. **Self-issued identity**: The Orchestrator acts as its own AP per spec §Self-Hosted Agents — it publishes agent metadata at `/.well-known/aauth-agent.json` and self-signs agent tokens with its published key. 2. **Per-request consent grant**: Grants consent for itself at the PS before each downstream call (demo simplification). 3. **Fallback path**: If the caller used HWK/JWKS-URI (no upstream auth token), falls back to standard challenge handling without chaining. From 9905e3e9566a3816051e1a94d58869e4a30edbb5 Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Tue, 26 May 2026 20:11:02 +0000 Subject: [PATCH 2/3] docs: fix remaining enrollment references for spec accuracy - README.md: three-party example now shows self-issued path (hosted services), not AP enrollment - docs/reference/dependency-injection.md: Key Principle section now explains both deployment models (hosted self-issue vs CLI/desktop AP enrollment) with separate Mermaid diagrams; add self-issued DI registration example alongside the AP-enrolled one - docs/README.md: scope BootstrapBuilder and AgentProviderClient descriptions to CLI/desktop agents --- README.md | 23 +++++++----- docs/README.md | 4 +-- docs/reference/dependency-injection.md | 49 ++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 27777c8..0e79a60 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,25 @@ var response = await client.GetAsync("https://resource.example/data"); ### Three-Party Flow (Agent → Resource → Person Server) -Enrollment is a one-time provisioning step (like a DB migration). The durable signing key lives in a keystore and is referenced by ID — never extracted. The agent token is short-lived and refreshed automatically: +Hosted services self-issue agent tokens (no external AP needed). CLI/desktop agents enrol with an Agent Provider instead. ```csharp -using AAuth.Agent; +using AAuth.Crypto; using AAuth.HttpSig; +using AAuth.Tokens; -// The key lives in the keystore; load it by the ID assigned during enrollment -var keyStore = KeyStore.Default(); // ~/.aauth/keys/ (or plug in HSM/TPM/Key Vault) -var key = await keyStore.LoadAsync("my-agent-key"); +// Hosted service: generate key at startup, self-issue tokens +var key = AAuthKey.Generate(); -using var client = new AAuthClientBuilder(key!) - .WithTokenRefresh(async (ctx, ct) => - await new AgentProviderClient(new HttpClient(), keyStore) - .RefreshAsync("https://ap.example/refresh", ctx.KeyId, ct)) +using var client = new AAuthClientBuilder(key) + .WithTokenRefresh(async (ctx, ct) => new AgentTokenBuilder + { + Issuer = "https://my-service.example", + Subject = "aauth:my-service@my-service.example", + KeyId = "svc-key-1", + Key = key, + PersonServer = "https://ps.example", + }.Build()) .WithChallengeHandling("https://ps.example") .Build(); diff --git a/docs/README.md b/docs/README.md index c4afbdf..fe97fee 100644 --- a/docs/README.md +++ b/docs/README.md @@ -79,7 +79,7 @@ This is the documentation for the AAuth .NET SDK (`AAuth` NuGet package). It cov | `JwksUriSignatureKeyProvider` | `sig=jwks_uri` — JWKS-discoverable identity | | `JwtSignatureKeyProvider` | `sig=jwt` — agent/auth token inline | | `JktJwtSignatureKeyProvider` | `sig=jkt-jwt` — key rotation mode | -| `BootstrapBuilder` | Fluent builder for bootstrap/enrollment flows | +| `BootstrapBuilder` | Fluent builder for AP enrollment (CLI/desktop agents) | | `ChallengeHandlingOptions` | Options for automatic 401 challenge handling | | `InteractionHandlingOptions` | Options for deferred/interaction handling | @@ -92,7 +92,7 @@ This is the documentation for the AAuth .NET SDK (`AAuth` NuGet package). It cov | `InteractionHandler` | `DelegatingHandler` — handles 202 deferred/interaction | | `TokenExchangeClient` | Sends signed `POST /token` to the Person Server | | `DeferredPoller` | Polls the pending URL until auth_token or timeout | -| `AgentProviderClient` | Enrols with an Agent Provider (`POST /enrol`) | +| `AgentProviderClient` | Enrols with an Agent Provider (CLI/desktop agents; hosted services self-issue) | | `IKeyStore` / `InMemoryKeyStore` | Key persistence abstraction | | `IInteractionPresenter` | Surface interaction URLs to the user | | `IPlatformAttestor` | Platform attestation hook | diff --git a/docs/reference/dependency-injection.md b/docs/reference/dependency-injection.md index 5205174..52b1f8c 100644 --- a/docs/reference/dependency-injection.md +++ b/docs/reference/dependency-injection.md @@ -4,18 +4,27 @@ Register AAuth services in ASP.NET Core and hosted applications using the built- ## Key Principle -Enrollment is a **provisioning step** — like a database migration or certificate issuance — that runs outside of your application's normal lifecycle. It generates a durable signing key **inside a keystore** (the private material never leaves) and registers the public key with the AP. Your application only needs the **key ID** to load the key at startup. +How your agent obtains its token depends on its deployment model: -The agent token is short-lived (typically 1 hour) and refreshed automatically by the SDK at runtime. You never persist it. +- **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. ```mermaid flowchart LR - P["Provisioning: EnrolAsync(keyStore)"] --> C["App config: AAuth:KeyId = key ID string"] - C --> S["App startup: keyStore.LoadAsync → AddAAuthAgent with TokenRefresher"] - S --> R["Runtime: SDK calls ITokenRefresher before token expires"] + 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:KeyId"] + C --> S["Startup: keyStore.LoadAsync → AddAAuthAgent"] + S --> R["Runtime: SDK calls AP refresh before expiry"] + end ``` -See [Bootstrap & Enrollment](../workflows/bootstrap-enrollment.md) for the provisioning step. +See [Bootstrap & Enrollment](../workflows/bootstrap-enrollment.md) for the CLI/desktop provisioning step, or [Getting Started](../getting-started.md#self-issued-agent-tokens-hosted-services) for the self-issued path. ## Agent Registration (Outbound Requests) @@ -33,7 +42,33 @@ builder.Services.AddAAuthAgent("signing-only", options => }); ``` -### Identity-Based (JWT) — With Challenge Handling +### Identity-Based (JWT) — Self-Issued (Hosted Services) + +No AP enrollment needed. The service generates a key and self-issues tokens: + +```csharp +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 = new SelfIssuedTokenRefresher(key, Kid, issuer, + agentId: "aauth:my-service@my-service.example", + personServer: "https://ps.example"); +}); + +// Also publish agent metadata so verifiers can discover the JWKS +app.MapAAuthAgentWellKnown(new AAuthAgentMetadataOptions +{ + Issuer = issuer, + SigningKeys = new Dictionary { [Kid] = key }, +}); +``` + +### Identity-Based (JWT) — AP-Enrolled (CLI/Desktop Agents) Load the key by ID from the store and configure token refresh: From f0183f7f533fc4868acaeee16ea27164a0bbd95a Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Tue, 26 May 2026 20:49:15 +0000 Subject: [PATCH 3/3] docs: audit all markdown against SDK API surface Validate code samples in 16 docs against the actual public API in src/AAuth/. Fix incorrect type names, property signatures, and patterns across server, signing-modes, workflows, reference, and advanced sections. Remove references to non-existent APIs and align DI option names with current source. --- README.md | 2 +- docs/advanced/key-management.md | 4 +- docs/reference/configuration.md | 40 +++++----- docs/reference/dependency-injection.md | 76 +++++++++++-------- docs/server/authorization-policies.md | 11 ++- docs/server/challenge-middleware.md | 23 ++++-- docs/server/multi-scheme-verification.md | 2 +- docs/server/replay-detection.md | 10 ++- docs/server/resource-metadata.md | 2 +- docs/server/verification-middleware.md | 18 ++--- docs/signing-modes/agent-identity-jwks-uri.md | 21 ++++- docs/signing-modes/agent-token-jwt.md | 4 +- docs/workflows/deferred-consent.md | 4 +- docs/workflows/federated-access.md | 12 ++- docs/workflows/ps-asserted-access.md | 12 ++- samples/README.md | 2 +- 16 files changed, 153 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 0e79a60..1d6735d 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ dotnet test tests/AAuth.Conformance # spec conformance suite only |------|-------------| | [src/AAuth/](src/AAuth/) | AAuth SDK library (the NuGet package) | | [docs/](docs/) | SDK documentation — signing modes, workflows, server guides | -| [samples/](samples/) | Sample applications — WhoAmI, AgentConsole, MockPersonServer, MockAgentProvider, GuidedTour, SampleApp | +| [samples/](samples/) | Sample applications — WhoAmI, Orchestrator, AgentConsole, MockPersonServer, MockAgentProvider, GuidedTour, SampleApp | | [tests/](tests/) | Unit, integration, and spec-conformance tests | | [aauth-spec/](aauth-spec/) | Protocol specifications (draft-01) from [dickhardt/AAuth](https://github.com/dickhardt/AAuth) | diff --git a/docs/advanced/key-management.md b/docs/advanced/key-management.md index 7cbc651..adc97d3 100644 --- a/docs/advanced/key-management.md +++ b/docs/advanced/key-management.md @@ -105,7 +105,7 @@ public sealed class AzureKeyVaultStore : IKeyStore try { var secret = await _client.GetSecretAsync(keyId, cancellationToken: ct); - return AAuthKey.FromJwk(secret.Value.Value); + return AAuthKey.FromJwkJson(secret.Value.Value); } catch (RequestFailedException ex) when (ex.Status == 404) { @@ -115,7 +115,7 @@ public sealed class AzureKeyVaultStore : IKeyStore public async Task StoreAsync(string keyId, IAAuthKey key, CancellationToken ct) { - var jwk = ((AAuthKey)key).ExportJwk(includePrivate: true); + var jwk = ((AAuthKey)key).ToPrivateJwk().ToJsonString(); await _client.SetSecretAsync(new KeyVaultSecret(keyId, jwk), ct); } diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 2ad21cb..14da0c6 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -31,11 +31,11 @@ All configurable options across the AAuth .NET SDK, grouped by component. |----------|------|---------|-------------| | `Issuer` | `string` | — (required) | HTTPS issuer URL for this resource | | `SigningKeys` | `Dictionary` | `{}` | Key-id → signing key map | -| `MaxSignatureAge` | `TimeSpan` | 60 seconds | Maximum allowed age of inbound signatures | -| `MaxFutureSkew` | `TimeSpan` | 5 seconds | Future skew tolerance for signature timestamps | -| `Clock` | `Func?` | `null` (UtcNow) | Clock source (threaded to `AAuthVerifier`) | -| `EnableReplayDetection` | `bool` | `true` | Enable JTI-based replay detection | -| `KeyResolver` | `ISignatureKeyResolver?` | `null` | Custom key resolver (null = default) | +| `ClientName` | `string?` | `null` | Human-readable resource name | +| `ScopeDescriptions` | `Dictionary?` | `null` | Scope → description map for metadata | +| `SignatureWindow` | `int?` | `null` | Advertised signature validity (seconds) | +| `AuthorizationEndpoint` | `string?` | `null` | AS authorization URL | +| `RevocationEndpoint` | `string?` | `null` | Revocation endpoint URL | ## Token Builders @@ -179,33 +179,37 @@ Standard `DelegatingHandler` — no configurable options. Requires an `ISignatur | Property | Type | Required | Description | |----------|------|:--------:|-------------| | `Key` | `IAAuthKey` | Yes | Agent signing key | -| `AgentToken` | `string?` | No | Agent token JWT (enables jwt mode) | -| `PersonServer` | `string?` | No | Person Server URL (enables challenge handling) | -| `OnInteractionRequired` | `Func<..., Task>?` | No | Callback for deferred consent interaction | -| `OnResourceInteraction` | `Func<..., Task>?` | No | Callback for resource-managed interaction | -| `OnApprovalPending` | `Func?` | No | Callback during approval polling | +| `BaseAddress` | `Uri?` | No | Target resource URL | +| `SignatureKeyProvider` | `ISignatureKeyProvider?` | No | Custom signature key provider | +| `PersonServer` | `string?` | No | Person Server URL (for challenge handling) | +| `ChallengeHandling` | `bool` | No | Enable challenge handling | +| `ChallengeHandlingOptions` | `Action?` | No | Configure challenge handling behavior | +| `InteractionHandling` | `bool` | No | Enable interaction handling | +| `InteractionHandlingOptions` | `Action?` | No | Configure interaction handling behavior | | `TokenRefresher` | `ITokenRefresher?` | No | Custom token refresh logic | -| `PollingTimeout` | `TimeSpan?` | No | Max deferred polling time (default: 5 min) | +| `RefreshThreshold` | `TimeSpan?` | No | Time before expiry to trigger refresh | +| `Capabilities` | `string[]?` | No | Agent capabilities to advertise | +| `InnerHandler` | `HttpMessageHandler?` | No | Custom inner HTTP handler | +| `CallChainProvider` | `Func?` | No | Provider for upstream auth token (call chaining) | ### AAuthResourceOptions (AddAAuthResource) | Property | Type | Required | Description | |----------|------|:--------:|-------------| | `Issuer` | `string` | Yes | Resource canonical URL | -| `SigningKeys` | `List<(string Kid, IAAuthKey Key)>` | Yes | Signing key pairs | -| `MaxSignatureAge` | `TimeSpan?` | No | Override verifier MaxAge | -| `EnableReplayDetection` | `bool` | No | Register `IJtiStore` (default: false) | -| `KeyResolver` | `ISignatureKeyResolver?` | No | Custom resolver | +| `SigningKeys` | `Dictionary` | Yes | Key-id → signing key map | | `ClientName` | `string?` | No | Resource display name | | `ScopeDescriptions` | `Dictionary?` | No | Scope descriptions for metadata | +| `SignatureWindow` | `int?` | No | Advertised signature validity (seconds) | +| `AuthorizationEndpoint` | `string?` | No | AS authorization URL | +| `RevocationEndpoint` | `string?` | No | Revocation endpoint URL | ### AAuthDiscoveryOptions (AddAAuthDiscovery) | Property | Type | Default | Description | |----------|------|---------|-------------| -| `MetadataCacheTtl` | `TimeSpan` | 5 minutes | Metadata document cache lifetime | -| `JwksCacheTtl` | `TimeSpan` | 1 hour | JWKS cache lifetime | -| `JwksMinRefreshInterval` | `TimeSpan` | 1 minute | Minimum time between JWKS fetches (spec: ≥1 min) | +| `MetadataCacheDuration` | `TimeSpan` | 5 minutes | Metadata document cache lifetime | +| `JwksCacheDuration` | `TimeSpan` | 5 minutes | JWKS cache lifetime | ### ChallengeHandlingOptions (WithChallengeHandling) diff --git a/docs/reference/dependency-injection.md b/docs/reference/dependency-injection.md index 52b1f8c..8fba7c8 100644 --- a/docs/reference/dependency-injection.md +++ b/docs/reference/dependency-injection.md @@ -38,7 +38,7 @@ var key = AAuthKey.Generate(); // or load from persistent storage builder.Services.AddAAuthAgent("signing-only", options => { options.Key = key; - // No AgentToken → defaults to HWK mode + options.UseHwk(); }); ``` @@ -55,9 +55,14 @@ builder.Services.AddAAuthAgent("self-issued", options => { options.Key = key; options.PersonServer = "https://ps.example"; - options.TokenRefresher = new SelfIssuedTokenRefresher(key, Kid, issuer, - agentId: "aauth:my-service@my-service.example", - personServer: "https://ps.example"); + options.UseJwt(() => new AgentTokenBuilder + { + Issuer = issuer, + Subject = "aauth:my-service@my-service.example", + KeyId = Kid, + Key = key, + PersonServer = "https://ps.example", + }.Build()); }); // Also publish agent metadata so verifiers can discover the JWKS @@ -107,13 +112,16 @@ builder.Services.AddAAuthAgent("interactive", options => options.Key = key; options.PersonServer = "https://ps.example"; options.TokenRefresher = new ApTokenRefresher(apRefreshEndpoint, keyStore, keyId); - options.OnInteractionRequired = async (interaction, ct) => + options.InteractionHandling = true; + options.InteractionHandlingOptions = io => { - // Present interaction.UserUrl and interaction.Code to user - logger.LogInformation("Approve at {Url} with code {Code}", - interaction.UserUrl, interaction.Code); + 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); }; - options.PollingTimeout = TimeSpan.FromMinutes(3); }); ``` @@ -142,7 +150,6 @@ builder.Services.AddAAuthResource(options => { options.Issuer = "https://my-resource.example"; options.SigningKeys = new() { ["key-1"] = resourceKey }; - options.EnableReplayDetection = true; // JTI-based (default) }); var app = builder.Build(); @@ -166,16 +173,16 @@ builder.Services.AddAAuthResource(options => }); ``` -### Custom Key Resolver +### Custom Authorization Endpoint -Override the default resolver for advanced scenarios (e.g., restricted schemes): +Override the authorization endpoint for advanced scenarios (e.g., custom access server): ```csharp builder.Services.AddAAuthResource(options => { options.Issuer = "https://my-resource.example"; options.SigningKeys = new() { ["key-1"] = resourceKey }; - options.KeyResolver = new DefaultSignatureKeyResolver(jwksClient); + options.AuthorizationEndpoint = "https://as.example/authorize"; }); ``` @@ -186,9 +193,8 @@ Register shared `MetadataClient` and `JwksClient` singletons with custom cache s ```csharp builder.Services.AddAAuthDiscovery(options => { - options.MetadataCacheTtl = TimeSpan.FromMinutes(10); - options.JwksCacheTtl = TimeSpan.FromHours(2); - options.JwksMinRefreshInterval = TimeSpan.FromMinutes(1); // spec minimum + options.MetadataCacheDuration = TimeSpan.FromMinutes(10); + options.JwksCacheDuration = TimeSpan.FromHours(2); }); ``` @@ -282,12 +288,18 @@ app.Run(); | Property | Type | Default | Description | |----------|------|---------|-------------| | `Key` | `IAAuthKey` | required | Agent signing key (must have private component) | -| `PersonServer` | `string?` | `null` | PS URL; with TokenRefresher, enables challenge handling | -| `OnInteractionRequired` | `Func<...>?` | `null` | Callback for user interaction prompts | -| `OnResourceInteraction` | `Func<...>?` | `null` | Callback for resource-initiated interaction | -| `OnApprovalPending` | `Func<...>?` | `null` | Callback for approval-pending state | +| `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?` | `null` | Configure challenge handling behavior | +| `InteractionHandling` | `bool` | `false` | Enable interaction handling | +| `InteractionHandlingOptions` | `Action?` | `null` | Configure interaction handling behavior | | `TokenRefresher` | `ITokenRefresher?` | `null` | Auto-refresh before token expiry | -| `PollingTimeout` | `TimeSpan` | 5 min | Max time to poll for deferred responses | +| `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?` | `null` | Provider for upstream auth token (call chaining) | ### AAuthResourceOptions @@ -295,21 +307,18 @@ app.Run(); |----------|------|---------|-------------| | `Issuer` | `string` | required | Resource HTTPS URL (metadata + audience) | | `SigningKeys` | `Dictionary` | empty | Keys for signing resource tokens | -| `MaxSignatureAge` | `TimeSpan` | 60s | Max inbound signature age | -| `MaxFutureSkew` | `TimeSpan` | 5s | Future clock skew tolerance | -| `Clock` | `Func?` | `null` | Clock source (null = UtcNow) | -| `EnableReplayDetection` | `bool` | `true` | JTI-based replay protection | -| `KeyResolver` | `ISignatureKeyResolver?` | `null` | Custom resolver (null = default) | | `ClientName` | `string?` | `null` | Human-readable name in metadata | | `ScopeDescriptions` | `Dictionary?` | `null` | Scope descriptions in metadata | +| `SignatureWindow` | `int?` | `null` | Advertised signature validity (seconds) | +| `AuthorizationEndpoint` | `string?` | `null` | AS authorization URL | +| `RevocationEndpoint` | `string?` | `null` | Revocation endpoint URL | ### AAuthDiscoveryOptions | 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 | -| `JwksMinRefreshInterval` | `TimeSpan` | 1 min | Minimum time between JWKS fetches | +| `MetadataCacheDuration` | `TimeSpan` | 5 min | How long to cache well-known metadata | +| `JwksCacheDuration` | `TimeSpan` | 5 min | How long to cache JWKS documents | ## Call Chaining (AAuthClientBuilder) @@ -318,19 +327,22 @@ For intermediary services that act as both resource and agent, `AAuthClientBuild ```csharp // From HttpContext (reads UpstreamAuthTokenFeature set by middleware) var client = new AAuthClientBuilder(key) - .WithTokenRefresh(refreshFunc) + .UseJwt(() => tokenHolder.Token) + .WithTokenRefresh(refresher) .WithCallChaining(httpContext) .Build(); // From a raw upstream token string var client = new AAuthClientBuilder(key) - .WithTokenRefresh(refreshFunc) + .UseJwt(() => tokenHolder.Token) + .WithTokenRefresh(refresher) .WithCallChaining(upstreamAuthToken) .Build(); // From a dynamic provider var client = new AAuthClientBuilder(key) - .WithTokenRefresh(refreshFunc) + .UseJwt(() => tokenHolder.Token) + .WithTokenRefresh(refresher) .WithCallChaining(() => GetUpstreamToken()) .Build(); ``` diff --git a/docs/server/authorization-policies.md b/docs/server/authorization-policies.md index 6f935fc..3d3aff8 100644 --- a/docs/server/authorization-policies.md +++ b/docs/server/authorization-policies.md @@ -7,13 +7,16 @@ The AAuth SDK integrates with ASP.NET Core's authorization system via `AAuthScop ```csharp using AAuth.DependencyInjection; -builder.Services.AddAuthorization(); +builder.Services.AddAAuthAuthentication(); builder.Services.AddAAuthAuthorization(); ``` -`AddAAuthAuthorization()` registers: +`AddAAuthAuthentication()` registers: - `AAuthAuthenticationHandler` as the default authentication scheme + +`AddAAuthAuthorization()` registers: - `AAuthScopeHandler` as an `IAuthorizationHandler` +- Built-in policies: `AAuth.Authenticated`, `AAuth.Identified`, `AAuth.Authorized` ## Scope-Based Policies @@ -77,6 +80,7 @@ builder.Services.AddAuthorization(options => options.AddPolicy("DataRead", policy => policy.Requirements.Add(new AAuthScopeRequirement("data:read"))); }); +builder.Services.AddAAuthAuthentication(); builder.Services.AddAAuthAuthorization(); var app = builder.Build(); @@ -84,6 +88,9 @@ app.UseAAuthVerification(); app.UseAAuthChallenge(new ChallengeOptions { AccessMode = AAuthAccessMode.RequireAuthToken, + ResourceSigningKey = resourceKey, + ResourceKeyId = "key-1", + ResourceIdentifier = "https://resource.example", }); app.UseAuthorization(); diff --git a/docs/server/challenge-middleware.md b/docs/server/challenge-middleware.md index c52bb90..3a13cde 100644 --- a/docs/server/challenge-middleware.md +++ b/docs/server/challenge-middleware.md @@ -39,22 +39,29 @@ public enum AAuthAccessMode ## Challenge Options ```csharp -public class ChallengeOptions +public sealed class ChallengeOptions { // How to handle access decisions - public AAuthAccessMode AccessMode { get; set; } = AAuthAccessMode.RequireAuthToken; + public AAuthAccessMode AccessMode { get; init; } = AAuthAccessMode.RequireAuthToken; // Resource signing key for minting resource tokens - public IAAuthKey? ResourceKey { get; set; } + public AAuthKey? ResourceSigningKey { get; init; } - // Resource identifier (audience in resource tokens) - public string? ResourceIdentifier { get; set; } + // Key identifier for the resource signing key (kid in the resource token header) + public string? ResourceKeyId { get; init; } - // Scopes to request in the challenge - public string? RequiredScope { get; set; } + // 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 IReadOnlyList? AllowedSchemes { get; set; } + public IReadOnlySet? AllowedSignatureKeySchemes { get; init; } } ``` diff --git a/docs/server/multi-scheme-verification.md b/docs/server/multi-scheme-verification.md index 4af4f95..8af0390 100644 --- a/docs/server/multi-scheme-verification.md +++ b/docs/server/multi-scheme-verification.md @@ -84,7 +84,7 @@ After resolution, the parsed info is available via `HttpContext.Items[AAuthVerif public sealed class ParsedSignatureKeyInfo { public required string Scheme { get; init; } // "hwk", "jwks_uri", "jwt", "jkt_jwt" - public AAuthKey? ConfirmationKey { get; init; } // resolved public key + public IAAuthKey? ConfirmationKey { get; init; } // resolved public key public string? Jkt { get; init; } // key thumbprint public string? JwksUri { get; init; } // declared JWKS URI (jwks_uri scheme) public string? Kid { get; init; } // key ID (jwks_uri scheme) diff --git a/docs/server/replay-detection.md b/docs/server/replay-detection.md index 062bcc6..888371e 100644 --- a/docs/server/replay-detection.md +++ b/docs/server/replay-detection.md @@ -109,13 +109,15 @@ using AAuth.Server; app.MapAAuthRevocationEndpoint(jtiStore, path: "/revoke"); ``` -This maps `POST /revoke` accepting: +This maps `POST /revoke` accepting form-encoded data: -```json -{ "jti": "token-id-to-revoke" } ``` +Content-Type: application/x-www-form-urlencoded -The endpoint calls `jtiStore.RevokeAsync(jti)` and returns `204 No Content`. +token=token-id-to-revoke +``` + +The endpoint calls `jtiStore.RevokeAsync(token)` and returns `200 OK`. Advertise it in resource metadata: diff --git a/docs/server/resource-metadata.md b/docs/server/resource-metadata.md index 2739394..18bc175 100644 --- a/docs/server/resource-metadata.md +++ b/docs/server/resource-metadata.md @@ -14,7 +14,7 @@ using AAuth.DependencyInjection; builder.Services.AddAAuthResource(options => { options.Issuer = "https://resource.example"; - options.SigningKeys = [("key-1", signingKey)]; + options.SigningKeys = new() { ["key-1"] = signingKey }; options.ClientName = "My Resource API"; options.ScopeDescriptions = new() { diff --git a/docs/server/verification-middleware.md b/docs/server/verification-middleware.md index 01a0ca4..3019a04 100644 --- a/docs/server/verification-middleware.md +++ b/docs/server/verification-middleware.md @@ -35,32 +35,32 @@ app.UseAAuthVerification(new AAuthVerificationOptions ## Options ```csharp -public class AAuthVerificationOptions +public sealed class AAuthVerificationOptions { // The resource's own identifier (used for audience checks). // When null, audience validation is skipped entirely. - public string? ResourceIdentifier { get; set; } + public string? ResourceIdentifier { get; init; } // Whether to verify JWT signatures against the issuer's JWKS (default: true) - public bool RequireIssuerVerification { get; set; } = true; + public bool RequireIssuerVerification { get; init; } = true; // Optional allow-list of trusted agent provider issuers - public IReadOnlySet? TrustedAgentProviderIssuers { get; set; } + public IReadOnlySet? TrustedAgentProviderIssuers { get; init; } // Optional allow-list of trusted auth token issuers (PS/AS) - public IReadOnlySet? TrustedAuthTokenIssuers { get; set; } + public IReadOnlySet? TrustedAuthTokenIssuers { get; init; } // Maximum depth of nested act claims (default: 10) - public int MaxActDepth { get; set; } = 10; + public int MaxActDepth { get; init; } = 10; // Tolerance for exp/iat validation (default: 30s) - public TimeSpan ClockSkew { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan ClockSkew { get; init; } = TimeSpan.FromSeconds(30); // Maximum future skew for HTTP signature timestamps (default: 5s) - public TimeSpan MaxFutureSkew { get; set; } = TimeSpan.FromSeconds(5); + public TimeSpan MaxFutureSkew { get; init; } = TimeSpan.FromSeconds(5); // Clock source for all time checks (null = UtcNow; inject for testing) - public Func? Clock { get; set; } + public Func? Clock { get; init; } } ``` diff --git a/docs/signing-modes/agent-identity-jwks-uri.md b/docs/signing-modes/agent-identity-jwks-uri.md index 5ef2e78..58fca7d 100644 --- a/docs/signing-modes/agent-identity-jwks-uri.md +++ b/docs/signing-modes/agent-identity-jwks-uri.md @@ -8,10 +8,29 @@ The agent references its JWKS endpoint and key ID. The resource fetches the publ - Access control by agent identity (the resource knows WHO is calling) - Replacing static API keys with verifiable cryptographic identity -- Requires an Agent Provider that publishes per-agent JWKS endpoints +- Requires a JWKS endpoint — either self-hosted (hosted services) or published by an Agent Provider (CLI/desktop agents) ## Code Example +**Hosted service (self-hosted JWKS):** + +```csharp +using AAuth.Crypto; +using AAuth.HttpSig; + +var key = AAuthKey.Generate(); + +// Hosted services publish their own JWKS at a stable URL. +// The resource fetches this URL to verify the agent's signature. +using var client = new AAuthClientBuilder(key) + .UseJwksUri("https://my-service.example/.well-known/jwks.json", "svc-key-1") + .Build(); + +var response = await client.GetAsync("https://resource.example/data"); +``` + +**CLI/Desktop agent (AP-enrolled):** + ```csharp using AAuth.Crypto; using AAuth.HttpSig; diff --git a/docs/signing-modes/agent-token-jwt.md b/docs/signing-modes/agent-token-jwt.md index 7f2b5c3..ca77020 100644 --- a/docs/signing-modes/agent-token-jwt.md +++ b/docs/signing-modes/agent-token-jwt.md @@ -24,14 +24,14 @@ using AAuth.Tokens; var key = AAuthKey.Generate(); using var client = new AAuthClientBuilder(key) - .WithTokenRefresh(async (ctx, ct) => new AgentTokenBuilder + .WithTokenRefresh((ctx, ct) => Task.FromResult(new AgentTokenBuilder { Issuer = "https://my-service.example", Subject = "aauth:my-service@my-service.example", KeyId = "svc-key-1", Key = key, PersonServer = "https://ps.example", - }.Build()) + }.Build())) .WithChallengeHandling("https://ps.example") .Build(); diff --git a/docs/workflows/deferred-consent.md b/docs/workflows/deferred-consent.md index 5025141..b859eb5 100644 --- a/docs/workflows/deferred-consent.md +++ b/docs/workflows/deferred-consent.md @@ -98,7 +98,7 @@ builder.Services.AddAAuthAgent("deferred", options => options.OnInteractionRequired = async (interaction, ct) => { // Present to user — push notification, SignalR, etc. - await notifier.SendAsync(interaction.UserUrl, interaction.Code, ct); + await notifier.SendAsync(interaction.Url, interaction.Code, ct); }; }); ``` @@ -148,7 +148,7 @@ class BrowserPresenter : IInteractionPresenter | Property | Default | Description | |----------|---------|-------------| | `MaxTotalWait` | 5 minutes | Maximum total polling time before timeout | -| `DefaultPollInterval` | 1 second | Time between polls (server may override via Retry-After) | +| `DefaultPollInterval` | 5 seconds | Time between polls (server may override via Retry-After) | | `MinPollInterval` | 100ms | Floor for poll interval | ## Error Scenarios diff --git a/docs/workflows/federated-access.md b/docs/workflows/federated-access.md index f6b5ed9..2213bc6 100644 --- a/docs/workflows/federated-access.md +++ b/docs/workflows/federated-access.md @@ -51,12 +51,18 @@ builder.Services.AddAAuthAgent("federated", options => { options.Key = key!; options.PersonServer = "https://ps.example"; - options.TokenRefresher = new DelegateTokenRefresher(async (ctx, ct) => + options.TokenRefresher = new ApTokenRefresher(keyStore, apRefreshEndpoint); +}); + +// ITokenRefresher implementation for AP-based refresh +class ApTokenRefresher(IKeyStore keyStore, string apRefreshEndpoint) : ITokenRefresher +{ + public async Task RefreshAsync(TokenRefreshContext ctx, CancellationToken ct) { var apClient = new AgentProviderClient(new HttpClient(), keyStore); return await apClient.RefreshAsync(apRefreshEndpoint, ctx.KeyId, ct); - }); -}); + } +} ``` See [Dependency Injection](../reference/dependency-injection.md) for full reference. diff --git a/docs/workflows/ps-asserted-access.md b/docs/workflows/ps-asserted-access.md index e7275f7..26942d4 100644 --- a/docs/workflows/ps-asserted-access.md +++ b/docs/workflows/ps-asserted-access.md @@ -127,12 +127,18 @@ builder.Services.AddAAuthAgent("ps-asserted", options => { options.Key = key!; options.PersonServer = "https://ps.example"; - options.TokenRefresher = new DelegateTokenRefresher(async (ctx, ct) => + options.TokenRefresher = new ApTokenRefresher(keyStore, apRefreshEndpoint); +}); + +// ITokenRefresher implementation for AP-based refresh +class ApTokenRefresher(IKeyStore keyStore, string apRefreshEndpoint) : ITokenRefresher +{ + public async Task RefreshAsync(TokenRefreshContext ctx, CancellationToken ct) { var apClient = new AgentProviderClient(new HttpClient(), keyStore); return await apClient.RefreshAsync(apRefreshEndpoint, ctx.KeyId, ct); - }); -}); + } +} ``` 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. diff --git a/samples/README.md b/samples/README.md index 9bdefce..915ed06 100644 --- a/samples/README.md +++ b/samples/README.md @@ -121,7 +121,7 @@ make restore # restore NuGet packages make test # run all tests (SDK + conformance) make test-unit # SDK unit + integration tests only make test-conformance # spec conformance tests only -make demo # start WhoAmI + MockPersonServer + MockAgentProvider + GuidedTour +make demo # start WhoAmI + Orchestrator + MockPersonServer + MockAgentProvider + GuidedTour make whoami # only the resource server (port 5000) make ps # MockPersonServer (port 5100) make ps-consent # MockPersonServer with RequireConsent=true