From 1cc7e953a173aefd1edff51b87390f9c6345388c Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Thu, 28 May 2026 08:13:39 +0000 Subject: [PATCH 1/5] docs: add three-party flow examples with mermaid diagrams - README: Add mermaid sequence diagram showing PS-Asserted flow with user consent, self-hosted agent setup, resource-side verification example, agent-calls-resource example, and step-by-step walk-through - Getting Started: Add protocol participants section (AP role explained), key types & cryptography, supported flows table, three-party deep dive with detailed HTTP headers and consent model, resource-side example with TrustedAuthTokenIssuers, and enrollment comparison table - Both files: Show PS trust relationship explicitly via TrustedAuthTokenIssuers in resource verification options --- .../implementation-plan.md | 169 +++++++++++ .../research.md | 134 +++++++++ README.md | 101 ++++++- docs/getting-started.md | 280 +++++++++++++++++- 4 files changed, 675 insertions(+), 9 deletions(-) create mode 100644 .agent/plans/2026-05-28-readme-getting-started-update/implementation-plan.md create mode 100644 .agent/plans/2026-05-28-readme-getting-started-update/research.md diff --git a/.agent/plans/2026-05-28-readme-getting-started-update/implementation-plan.md b/.agent/plans/2026-05-28-readme-getting-started-update/implementation-plan.md new file mode 100644 index 0000000..932d752 --- /dev/null +++ b/.agent/plans/2026-05-28-readme-getting-started-update/implementation-plan.md @@ -0,0 +1,169 @@ +# Implementation Plan: README & Getting Started Documentation Update + +## Phase 1: README.md — Three-Party Flow with Mermaid Diagram + +### Changes + +**File:** `README.md` + +Replace the existing "Three-Party Flow" section with an expanded version including: + +1. **Mermaid sequence diagram** showing the PS-Asserted (three-party) flow with user consent: + +```mermaid +sequenceDiagram + participant Agent + participant Resource + participant PS as Person Server + participant User + + Agent->>Resource: GET /data (signed, agent token) + Resource-->>Agent: 401 + resource_token (aud=PS) + Agent->>PS: POST /token (signed, resource_token) + PS->>User: Consent prompt (scope, justification) + User-->>PS: Grant consent + PS-->>Agent: auth_token (aa-auth+jwt) + Agent->>Resource: GET /data (signed, auth_token) + Resource-->>Agent: 200 OK +``` + +2. **Self-hosted agent setup** — concise code showing: + - Key generation + - Publishing agent metadata (`MapAAuthAgentWellKnown`) + - Self-issuing agent tokens via `AgentTokenBuilder` + - Building the signed client with challenge handling + +3. **Resource-side example** — concise code showing: + - Verification middleware registration + - Resource metadata publication + - Issuing resource tokens (challenge response) + +4. **Agent calling the resource** — code showing the full client call and a brief "what happens" explanation + +5. **Brief walk-through** of the exchange (numbered list): + 1. Agent signs GET with agent token → Resource verifies signature, reads `ps` from agent token + 2. Resource returns 401 with a `resource_token` (audience = PS URL) + 3. Agent POSTs resource_token to PS's token endpoint (signed request) + 4. PS validates agent token, prompts user for consent + 5. User grants consent; PS issues `auth_token` with identity claims + 6. Agent retries original request signed with auth token + 7. Resource verifies auth token signature + claims → 200 OK + +### Definition of Done + +- [x] Mermaid diagram renders correctly (3-party with user consent, 4 participants) +- [x] Self-hosted agent code example compiles conceptually (uses real SDK types) +- [x] Resource-side code example uses `UseAAuthVerification`, `MapAAuthWellKnown`, `ResourceTokenBuilder` +- [x] Agent-calls-resource code example uses `AAuthClientBuilder` with `WithChallengeHandling` +- [x] Walk-through text uses spec-accurate terminology (Person Server not "auth server", resource_token not "challenge token", auth_token not "access token") +- [x] No bearer tokens mentioned — every credential is bound to a signing key +- [x] README remains concise — detailed explanations go in getting-started + +--- + +## Phase 2: Getting Started — Expanded Protocol & Enrollment Guide + +### Changes + +**File:** `docs/getting-started.md` + +Add/expand the following sections after the existing "What Just Happened?" section and before "Self-Issued Agent Tokens": + +#### 2a. "Understanding the Protocol Participants" section + +Expand on what each party does, with emphasis on the **Agent Provider (AP)**: + +- **What an AP does**: Issues agent tokens that bind a signing key to an agent identity. Acts as the trust anchor for agent identity. Analogous to a certificate authority for agents. +- **Self-hosted vs enrolled**: + - Self-hosted: Agent has stable URL → is its own AP → publishes `/.well-known/aauth-agent.json` → self-signs tokens. No external enrollment. Used by web apps, APIs, orchestrators. + - Enrolled (external AP): CLI/desktop/mobile agents register with an AP. AP holds public key, agent holds private key locally. Agent token refreshed automatically (SDK manages this). +- **Key separation**: Agent and AP never share a keystore. Agent holds private key in local `IKeyStore`; AP holds only the public key. + +#### 2b. "Key Types & Cryptography" section + +- **Ed25519** — required for signing keys (`AAuthKey.Generate()` produces Ed25519) +- **JWK Thumbprint (S256)** — how keys are identified without exposing the full key +- **JWT signing** — all AAuth tokens are Ed25519-signed JWTs +- Note: spec says EdDSA is RECOMMENDED; implementations MUST NOT accept `none` + +#### 2c. "Supported Flows" section + +Table + brief description of each flow with links: + +| Flow | Parties | When to Use | Signing Mode | +|------|---------|-------------|--------------| +| Identity-Based | Agent + Resource | API-key replacement, simple access control | `hwk` or `jwks_uri` | +| Resource-Managed (two-party) | Agent + Resource | Resource handles its own auth (interaction, OAuth) | Any | +| PS-Asserted (three-party) | Agent + Resource + PS | User consent required, resource delegates auth to PS | `jwt` | +| Federated (four-party) | Agent + Resource + PS + AS | Cross-domain policy, resource has its own AS | `jwt` | + +#### 2d. "Three-Party Flow Deep Dive" section + +Detailed walk-through with: + +1. **Mermaid diagram** (same as README but more detailed, showing consent interaction) +2. **Step-by-step explanation** of each message: + - What headers are sent + - What the resource checks + - What the PS validates + - How consent works (immediate vs deferred) + - What claims the auth token contains +3. **Self-hosted agent code** (full example with metadata publishing) +4. **Resource-side code** (full example with verification + token issuance) +5. **Client code** calling the resource (showing automatic challenge handling) + +#### 2e. "Enrollment: Hosted vs CLI/Desktop Agents" section + +Restructure existing enrollment content into a clear comparison: + +| Aspect | Self-Hosted (Web App/API) | Enrolled (CLI/Desktop) | +|--------|---------------------------|------------------------| +| AP needed? | No — agent IS its own AP | Yes — external AP | +| URL requirement | Stable HTTPS URL | None | +| Key lifecycle | Generated at startup, published via JWKS | Generated in keystore at enrollment, loaded by handle | +| Token acquisition | Self-signed at startup | AP refresh endpoint (automatic via SDK) | +| Metadata | Publishes `/.well-known/aauth-agent.json` | AP publishes it | +| Code entry point | `MapAAuthAgentWellKnown()` + `AgentTokenBuilder` | `AAuthClientBuilder.Bootstrap().EnrolAsync()` | + +### Definition of Done + +- [x] "Understanding the Protocol Participants" section explains AP role clearly +- [x] Self-hosted vs enrolled comparison table present +- [x] Key types section covers Ed25519, JWK thumbprint, JWT signing +- [x] Supported flows table covers all 4 modes with correct signing mode requirements +- [x] Three-party deep dive includes mermaid diagram with user consent +- [x] Step-by-step explanation covers headers, validation, consent (immediate + deferred) +- [x] Resource-side code example present (verification + metadata + token issuance) +- [x] All terminology matches spec exactly (Person Server, resource_token, auth_token, etc.) +- [x] No references to bearer tokens or OAuth concepts that don't apply +- [x] Links to detailed docs (signing-modes/overview, workflows/, server/) work correctly + +--- + +## Phase 3: Review & Cross-References + +### Changes + +1. Ensure README links to the new getting-started sections +2. Ensure getting-started links to workflow docs, server docs, and signing-modes docs +3. Verify no terminology drift between README, getting-started, and spec +4. Ensure code examples are consistent across both files (same patterns, same type names) + +### Definition of Done + +- [x] README "See Getting Started" link points to correct anchor +- [x] Getting-started "Next Steps" links all resolve +- [x] Terminology audit: no "bearer token", "access token" (use "auth token"), "authorization server" (use "Access Server" or "Person Server") +- [x] Code examples use current SDK API surface (builder pattern, real type names) + +--- + +## Out of Scope + +| Item | Reason | +|------|--------| +| Updating workflow docs (`docs/workflows/`) | Separate concern; already well-documented | +| Adding new sample projects | Documentation-only change | +| Spec conformance testing of examples | Examples are illustrative, not runnable | +| Mission/governance documentation | Orthogonal layer, not part of basic getting-started | +| Four-party (federated) detailed example | Complex; three-party is the focus | diff --git a/.agent/plans/2026-05-28-readme-getting-started-update/research.md b/.agent/plans/2026-05-28-readme-getting-started-update/research.md new file mode 100644 index 0000000..c98859f --- /dev/null +++ b/.agent/plans/2026-05-28-readme-getting-started-update/research.md @@ -0,0 +1,134 @@ +# Research: README & Getting Started Documentation Update + +## Objective + +Update the main `README.md` and `docs/getting-started.md` to include a clear three-party (PS-Asserted) flow example with a mermaid diagram, self-hosted agent setup, resource-side code, and an agent calling the resource. The getting started guide should also expand coverage of AP roles, enrollment models, key types, and supported flows. + +## Source Material + +### Spec Terminology (draft-hardt-oauth-aauth-protocol) + +| Term | Definition | +|------|------------| +| **Person** | User or organization on whose behalf an agent acts | +| **Agent** | HTTP client acting on behalf of a person; identified by `aauth:local@domain` URI | +| **Agent Provider (AP)** | Server managing agent identity; issues agent tokens binding key → identity; publishes `/.well-known/aauth-agent.json` | +| **Resource** | Protected API; verifies signatures, issues resource tokens; publishes `/.well-known/aauth-resource.json` | +| **Person Server (PS)** | Represents the person; manages consent, asserts identity, brokers authorization; publishes `/.well-known/aauth-person.json` | +| **Access Server (AS)** | Policy engine; issues auth tokens on behalf of a resource; publishes `/.well-known/aauth-access.json` | +| **Agent Token** | `aa-agent+jwt` — binds agent key → identity; issued by AP or self-issued | +| **Resource Token** | `aa-resource+jwt` — challenge from resource saying "get auth from my PS/AS" | +| **Auth Token** | `aa-auth+jwt` — proves user authorized this agent; issued by PS or AS | +| **Mission** | Scoped authorization context for governance (orthogonal to access modes) | + +### Resource Access Modes (spec §Protocol Overview) + +1. **Identity-Based** — Agent + Resource only. Resource trusts signed identity directly. +2. **Resource-Managed (two-party)** — Resource handles auth itself (interaction, OAuth, internal policy). +3. **PS-Asserted (three-party)** — Resource issues resource token (aud=PS) → agent exchanges at PS → gets auth token → presents to resource. +4. **Federated (four-party)** — Resource has its own AS; PS federates with AS. + +### PS-Asserted Flow (Three-Party) — Spec §PS-Asserted Access + +Sequence from spec: + +1. Agent sends signed request to resource (Signature-Key: sig=jwt with agent token) +2. Resource reads PS URL from `ps` claim in agent token +3. Resource returns 401 + `AAuth-Requirement: requirement=auth-token` with a `resource_token` (aud=PS URL) +4. Agent POSTs resource token to PS token endpoint (signed request) +5. PS validates agent token, confirms user consent (immediate or deferred) +6. PS returns `auth_token` (`aa-auth+jwt`) with identity claims (sub, email, etc.) +7. Agent retries original request with auth token in Signature-Key + +### Signing Modes (spec §Agent Identity + HTTP Signature Keys) + +| Mode | Scheme | Use Case | +|------|--------|----------| +| Pseudonymous | `sig=hwk` | Rate-limiting by key, no identity needed | +| Agent Identity | `sig=jwks_uri` | Identity-based access without PS flows | +| Agent Token | `sig=jwt` | Full PS/AS authorization flows | +| Key Rotation | `sig=jkt-jwt` | Naming JWT binds ephemeral key to stable identity | + +### Agent Token Acquisition (spec §Agent Token) + +Two models: +1. **Self-hosted agents** — Agent has stable URL, publishes own `/.well-known/aauth-agent.json`, self-signs tokens. No external AP. +2. **Enrolled agents** — CLI/desktop/mobile; register with external AP; AP issues tokens. + +Key facts from spec: +- Agent generates Ed25519 keypair +- Agent proves identity to AP (platform-specific mechanism) +- AP issues agent token binding public key to agent identifier +- Token lifetime: max 24 hours (spec: "SHOULD NOT exceed 24 hours") +- `cnf.jwk` in agent token contains agent's public key +- Optional `ps` claim identifies agent's person server + +### Self-Hosted Agent Details (spec §Roles + bootstrap spec) + +Per spec §Roles: "A self-hosted agent is its own agent provider, self-issuing agent tokens signed by a JWKS-published key the user controls." + +Requirements: +- Stable HTTPS URL +- Publish `/.well-known/aauth-agent.json` (metadata with `jwks_uri`) +- Self-sign agent tokens with private key +- JWKS endpoint publishes the public key + +### Resource-Side Verification (SDK) + +From SDK docs: +- `UseAAuthVerification()` middleware verifies HTTP signatures + JWT issuer +- `ResourceTokenBuilder` issues challenge tokens +- `AddAAuthResource()` DI registration +- `MapAAuthWellKnown()` serves discovery metadata + +### User Consent in Three-Party Flow + +From spec: "PS validates the agent token, confirms user consent (or defers), and returns an auth token." + +The PS can: +- Grant immediately (pre-authorized or auto-consent policy) +- Defer (202 Accepted with `requirement=interaction`) — agent polls until user consents +- Deny (403) + +The SDK `ChallengeHandler` handles the 401 → exchange → retry cycle automatically. For deferred consent, the handler also handles polling with `InteractionWaitMode`. + +## SDK Types Mapping + +| Concept | SDK Type | +|---------|----------| +| Key generation | `AAuthKey.Generate()` | +| Key storage | `IKeyStore`, `FileKeyStore` | +| Client builder | `AAuthClientBuilder` | +| HTTP signing | `AAuthSigningHandler` | +| Challenge handling | `ChallengeHandler` | +| Token exchange | `TokenExchangeClient` | +| Self-issued tokens | `AgentTokenBuilder`, `SelfIssuedTokenRefresher` | +| AP enrollment | `AAuthClientBuilder.Bootstrap()` | +| AP token refresh | `AgentProviderTokenRefresher` | +| Resource verification | `AAuthVerificationMiddleware`, `AAuthVerifier` | +| Resource tokens | `ResourceTokenBuilder` | +| Auth tokens | `AuthTokenBuilder` | +| Resource metadata | `AAuthResourceMetadataOptions`, `MapAAuthResourceWellKnown()` | +| Agent metadata | `AAuthAgentMetadataOptions`, `MapAAuthAgentWellKnown()` | +| DI | `AddAAuthAgent()`, `AddAAuthResource()` | + +## Existing Documentation Gaps + +### README.md + +- Three-party flow example exists but lacks a visual mermaid diagram +- No resource-side code example +- No end-to-end walk-through of what happens in the 3-party exchange +- AP role described only in a sidebar note + +### docs/getting-started.md + +- Self-hosted agent section exists but could be clearer about WHY self-hosting works (stable URL = own AP) +- Bootstrap/enrollment section exists but doesn't explain what an AP fundamentally does +- No resource-side setup example +- No overview of which flows are supported and when to use each +- Key types not explicitly enumerated (only Ed25519 mentioned) + +## Open Questions + +- None — spec, SDK, and existing docs provide sufficient detail. diff --git a/README.md b/README.md index 1d6735d..11d2c9f 100644 --- a/README.md +++ b/README.md @@ -47,38 +47,125 @@ var response = await client.GetAsync("https://resource.example/data"); ### Three-Party Flow (Agent → Resource → Person Server) -Hosted services self-issue agent tokens (no external AP needed). CLI/desktop agents enrol with an Agent Provider instead. +The PS-Asserted flow is the primary authorization model. The resource delegates authorization to the agent's Person Server, which prompts the user for consent: + +```mermaid +sequenceDiagram + participant Agent + participant Resource + participant PS as Person Server + participant User + + Agent->>Resource: GET /data (signed, agent token) + Resource-->>Agent: 401 + resource_token (aud=PS) + Agent->>PS: POST /token (signed, resource_token) + PS->>User: Consent prompt (scope, justification) + User-->>PS: Grant consent + PS-->>Agent: auth_token (aa-auth+jwt) + Agent->>Resource: GET /data (signed, auth_token) + Resource-->>Agent: 200 OK +``` + +#### Self-Hosted Agent (Server-Side) + +Hosted services act as their own Agent Provider — generate a key, publish metadata, and self-issue tokens: ```csharp using AAuth.Crypto; using AAuth.HttpSig; +using AAuth.Server; using AAuth.Tokens; -// Hosted service: generate key at startup, self-issue tokens var key = AAuthKey.Generate(); +const string Kid = "svc-key-1"; +var issuer = "https://my-service.example"; +// Publish agent metadata so resources can discover the JWKS +app.MapAAuthAgentWellKnown(new AAuthAgentMetadataOptions +{ + Issuer = issuer, + SigningKeys = new Dictionary { [Kid] = key }, +}); + +// Build signed client with automatic token refresh and challenge handling using var client = new AAuthClientBuilder(key) .WithTokenRefresh(async (ctx, ct) => new AgentTokenBuilder { - Issuer = "https://my-service.example", + Issuer = issuer, Subject = "aauth:my-service@my-service.example", - KeyId = "svc-key-1", + KeyId = Kid, Key = key, PersonServer = "https://ps.example", }.Build()) .WithChallengeHandling("https://ps.example") .Build(); +``` -var response = await client.GetAsync("https://resource.example/protected"); +#### Resource (Server-Side) + +The resource verifies signatures, publishes metadata, and issues resource tokens as challenges: + +```csharp +using AAuth.DependencyInjection; +using AAuth.Server; + +var resourceKey = AAuthKey.Generate(); + +// Register AAuth resource services +builder.Services.AddAAuthResource(options => +{ + options.Issuer = "https://resource.example"; + options.SigningKeys = new() { ["resource-key-1"] = resourceKey }; + options.ScopeDescriptions = new() { ["read"] = "Read your data" }; +}); + +var app = builder.Build(); + +// Serve /.well-known/aauth-resource.json + JWKS +app.MapAAuthWellKnown(); + +// Verify HTTP signatures and auth tokens from trusted Person Servers +app.UseAAuthVerification(new AAuthVerificationOptions +{ + ResourceIdentifier = "https://resource.example", + RequireIssuerVerification = true, + // Trust specific PSes — the resource verifies auth tokens against their JWKS. + // Omit to accept any PS (claims are namespaced by issuer per spec). + TrustedAuthTokenIssuers = new HashSet { "https://ps.example" }, +}); ``` -See [Getting Started](docs/getting-started.md) for key persistence, DI integration, and all signing modes. +When an agent presents an `auth_token`, the resource verifies its signature against the PS's published JWKS (discovered at `{iss}/.well-known/aauth-person.json`). The `TrustedAuthTokenIssuers` allow-list restricts which Person Servers the resource will accept auth tokens from. + +#### Agent Calls the Resource + +With `WithChallengeHandling`, the entire 401 → exchange → retry cycle is automatic: + +```csharp +var response = await client.GetAsync("https://resource.example/data"); +// 1. Agent signs GET with agent token → Resource verifies, returns 401 + resource_token +// 2. ChallengeHandler POSTs resource_token to PS token endpoint +// 3. PS validates agent, prompts user for consent, issues auth_token +// 4. Agent retries GET signed with auth_token → Resource verifies → 200 OK +``` + +**What happens step by step:** + +1. Agent signs the request with its agent token (`Signature-Key: sig=jwt;jwt="..."`) +2. Resource verifies the signature, reads the `ps` claim, returns `401` with a `resource_token` (audience = PS URL) +3. Agent POSTs the `resource_token` to the PS's token endpoint (signed request) +4. PS validates the agent token, prompts the user for consent on the requested scope +5. User grants consent; PS issues an `auth_token` (`aa-auth+jwt`) containing identity claims (`sub`, `email`, etc.) +6. Agent retries the original request signed with the `auth_token` +7. Resource verifies the auth token signature and claims → `200 OK` + +See [Getting Started](docs/getting-started.md#three-party-flow-deep-dive) for a detailed walk-through, including deferred consent and the resource-side token issuance code. ## Documentation Full SDK documentation lives in [`docs/`](docs/): -- [Getting Started](docs/getting-started.md) — install, generate a key, first signed request +- [Getting Started](docs/getting-started.md) — install, generate a key, three-party flow deep dive, enrollment models - [Concepts](docs/concepts.md) — the four participants and how the SDK maps to them - [Signing Modes](docs/signing-modes/overview.md) — hwk, jwks_uri, jwt, jkt-jwt - [Workflows](docs/workflows/identity-based-access.md) — identity-based, PS-asserted, federated diff --git a/docs/getting-started.md b/docs/getting-started.md index ddd17a6..1941d14 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -75,6 +75,280 @@ 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`. +## Understanding the Protocol Participants + +AAuth is a four-party protocol. Each party has a distinct role: + +| Role | What It Does | +|------|--------------| +| **Agent** | HTTP client acting on behalf of a person. Signs every request with its private key. Identified by `aauth:local@domain`. | +| **Resource** | Protected API. Verifies HTTP signatures, issues resource tokens as challenges, enforces access policy. | +| **Person Server (PS)** | Represents the user. Manages consent, asserts identity claims (`sub`, `email`, `tenant`), brokers authorization. | +| **Access Server (AS)** | Policy engine for a resource. Issues auth tokens. Used in federated (four-party) mode. | + +### The Agent Provider (AP) + +The **Agent Provider** is a supporting role that issues agent tokens (`aa-agent+jwt`) binding a signing key to an agent identity. It is the trust anchor for agent identity — analogous to a certificate authority, but for agents. + +Two deployment models exist: + +| Model | How It Works | Used By | +|-------|--------------|---------| +| **Self-hosted** | Agent has a stable HTTPS URL → acts as its own AP → publishes `/.well-known/aauth-agent.json` → self-signs tokens | Web apps, APIs, orchestrators | +| **Enrolled (external AP)** | Agent registers with an external AP that holds the public key and issues tokens | CLI tools, desktop apps, mobile apps | + +In both models, the agent holds the **private** signing key locally in its own keystore (`IKeyStore`). The AP holds only the **public** key. They never share a keystore. + +## Key Types & Cryptography + +AAuth uses a minimal set of cryptographic primitives: + +| Primitive | Purpose | SDK | +|-----------|---------|-----| +| **Ed25519** | Signing key for all HTTP signatures and JWT tokens | `AAuthKey.Generate()` | +| **JWK Thumbprint (S256)** | Compact key identifier — a SHA-256 hash of the canonical public key | `key.ComputeJwkThumbprint()` | +| **JWT (Ed25519-signed)** | All AAuth tokens (`aa-agent+jwt`, `aa-resource+jwt`, `aa-auth+jwt`) | `AgentTokenBuilder`, `ResourceTokenBuilder`, `AuthTokenBuilder` | + +The spec requires EdDSA (Ed25519) and prohibits the `none` algorithm. Every request is signed per RFC 9421 (HTTP Message Signatures) — there are no bearer tokens anywhere in the protocol. + +## Supported Flows + +AAuth supports four resource access modes. Each adds parties and capabilities: + +| Flow | Parties | When to Use | Signing Mode | +|------|---------|-------------|--------------| +| **Identity-Based** | Agent + Resource | API-key replacement, simple access control by identity | `hwk` or `jwks_uri` | +| **Resource-Managed** (two-party) | Agent + Resource | Resource handles its own auth (interaction, existing OAuth) | Any | +| **PS-Asserted** (three-party) | Agent + Resource + PS | User consent required, resource delegates auth to PS | `jwt` | +| **Federated** (four-party) | Agent + Resource + PS + AS | Cross-domain policy, resource has its own Access Server | `jwt` | + +Adoption is incremental — each party can add support independently, and modes build on each other. See [Signing Modes](signing-modes/overview.md) for details on each scheme. + +## Three-Party Flow Deep Dive + +The PS-Asserted flow is the most common authorization model. The resource issues a challenge; the agent exchanges it at the Person Server for an auth token with user consent. + +### Sequence + +```mermaid +sequenceDiagram + participant Agent + participant Resource + participant PS as Person Server + participant User + + Agent->>Resource: GET /data (Signature-Key: sig=jwt, agent token) + Resource->>Resource: Verify signature, read ps claim + Resource-->>Agent: 401 + AAuth-Requirement: resource_token (aud=PS) + + Agent->>PS: POST /token (signed, resource_token in body) + PS->>PS: Validate agent token (issuer JWKS, cnf, exp) + PS->>User: Consent prompt (scope, justification) + User-->>PS: Grant consent + PS-->>Agent: 200 + auth_token (aa-auth+jwt, claims: sub, email) + + Agent->>Resource: GET /data (Signature-Key: sig=jwt, auth token) + Resource->>Resource: Verify auth token (issuer JWKS, aud, cnf, scope) + Resource-->>Agent: 200 OK +``` + +### Step-by-Step Explanation + +**1. Agent → Resource (initial request)** + +The agent signs the request with its agent token: + +``` +GET /data HTTP/1.1 +Host: resource.example +Signature-Key: sig=jwt;jwt="" +Signature-Input: sig=("@method" "@authority" "@path" "signature-key");... +Signature: sig=:: +``` + +**2. Resource → Agent (401 challenge)** + +The resource verifies the HTTP signature, extracts the `ps` claim from the agent token, and issues a `resource_token` (`aa-resource+jwt`) with `aud` set to the PS URL: + +``` +HTTP/1.1 401 Unauthorized +AAuth-Requirement: requirement=auth-token;resource_token="" +``` + +The resource token contains: issuer (resource URL), audience (PS URL), agent identifier, agent key thumbprint (`jkt`), and requested scope. + +**3. Agent → Person Server (token exchange)** + +The agent POSTs the resource token to the PS's token endpoint (discovered via `/.well-known/aauth-person.json`): + +``` +POST /token HTTP/1.1 +Host: ps.example +Content-Type: application/json +Signature-Key: sig=jwt;jwt="" + +{"resource_token": ""} +``` + +**4. Person Server validates and prompts for consent** + +The PS: +- Verifies the agent token signature against the AP's published JWKS +- Verifies `cnf.jwk` matches the request's signing key +- Decodes the resource token and verifies it was issued by the resource (via resource JWKS) +- Prompts the user for consent on the requested scope + +**5. Consent: immediate vs deferred** + +- **Immediate**: User is online and grants consent in real time. PS returns the auth token directly. +- **Deferred**: User is not available. PS returns `202 Accepted` with `requirement=interaction` and a `pending` URL. The agent polls until the user consents (SDK handles this via `InteractionWaitMode`). + +**6. Person Server → Agent (auth token)** + +The PS issues an `auth_token` (`aa-auth+jwt`) containing: +- `iss`: PS URL +- `aud`: Resource URL +- `sub`: User identifier (stable, PS-scoped) +- `cnf.jkt`: Agent's key thumbprint (proof-of-possession binding) +- `scope`: Granted scope +- Optional identity claims: `email`, `tenant`, `groups`, `roles` + +**7. Agent → Resource (retry with auth token)** + +The agent retries the original request, now signed with the auth token: + +``` +GET /data HTTP/1.1 +Host: resource.example +Signature-Key: sig=jwt;jwt="" +Signature-Input: sig=("@method" "@authority" "@path" "signature-key");... +Signature: sig=:: +``` + +The resource verifies the auth token: +- Fetches the PS's JWKS (from `{iss}/.well-known/aauth-person.json`) and verifies the JWT signature +- Checks `aud` matches its own identifier +- Confirms `cnf.jkt` matches the signing key on the HTTP request (proof-of-possession) +- Evaluates the granted `scope` against the requested operation +- Optionally checks the issuer is in `TrustedAuthTokenIssuers` + +Per the spec, any PS can assert identity claims to any resource without bilateral setup — the resource namespaces claims by the PS's issuer URL (the same `sub` from a different PS is a different subject). Resources that want to restrict which PSes they accept set `TrustedAuthTokenIssuers`. + +### Self-Hosted Agent Example + +A hosted service (web app, API, orchestrator) acts as its own Agent Provider: + +```csharp +using AAuth.Crypto; +using AAuth.HttpSig; +using AAuth.Server; +using AAuth.Tokens; + +var builder = WebApplication.CreateBuilder(args); +var key = AAuthKey.Generate(); +const string Kid = "svc-key-1"; +var issuer = "https://my-service.example"; + +var app = builder.Build(); + +// Publish /.well-known/aauth-agent.json so resources can discover the JWKS +app.MapAAuthAgentWellKnown(new AAuthAgentMetadataOptions +{ + Issuer = issuer, + SigningKeys = new Dictionary { [Kid] = key }, +}); + +// Build a signed HTTP client with automatic token refresh and challenge handling +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, + PersonServer = "https://ps.example", + }.Build()) + .WithChallengeHandling("https://ps.example") + .Build(); + +// Every request is signed; 401 challenges are handled automatically +var response = await client.GetAsync("https://resource.example/data"); +``` + +### Resource-Side Example + +A resource that verifies signatures and issues resource token challenges: + +```csharp +using AAuth.Crypto; +using AAuth.DependencyInjection; +using AAuth.Server; + +var builder = WebApplication.CreateBuilder(args); +var resourceKey = AAuthKey.Generate(); + +// Register resource services (metadata + signing key) +builder.Services.AddAAuthResource(options => +{ + options.Issuer = "https://resource.example"; + options.SigningKeys = new() { ["resource-key-1"] = resourceKey }; + options.ScopeDescriptions = new() + { + ["read"] = "Read access to your documents", + ["write"] = "Write access to your documents", + }; +}); + +var app = builder.Build(); + +// Serve /.well-known/aauth-resource.json and JWKS endpoint +app.MapAAuthWellKnown(); + +// Verify HTTP signatures and auth tokens from trusted Person Servers +app.UseAAuthVerification(new AAuthVerificationOptions +{ + ResourceIdentifier = "https://resource.example", + RequireIssuerVerification = true, + // Restrict which Person Servers this resource trusts. + // The resource verifies auth tokens against the PS's JWKS + // (discovered at {iss}/.well-known/aauth-person.json). + // Omit to dynamically accept any PS — claims are namespaced by issuer. + TrustedAuthTokenIssuers = new HashSet { "https://ps.example" }, +}); + +// Protected endpoint — if agent lacks auth, middleware issues 401 + resource_token +app.MapGet("/data", (HttpContext ctx) => +{ + var agent = ctx.GetAAuthAgent(); // parsed from verified signature + return Results.Ok(new { message = $"Hello {agent.Subject}" }); +}); + +app.Run(); +``` + +### Agent Calling the Resource + +With the SDK's `ChallengeHandler`, the entire three-party exchange is automatic: + +```csharp +var response = await client.GetAsync("https://resource.example/data"); +Console.WriteLine(await response.Content.ReadAsStringAsync()); +// {"message":"Hello aauth:my-service@my-service.example"} +``` + +The `ChallengeHandler` intercepts the `401`, extracts the resource token, exchanges it at the PS, caches the resulting auth token, and retries — all transparently. + +## Enrollment: Hosted vs CLI/Desktop Agents + +| Aspect | Self-Hosted (Web App/API) | Enrolled (CLI/Desktop) | +|--------|---------------------------|------------------------| +| AP needed? | No — agent IS its own AP | Yes — external AP | +| URL requirement | Stable HTTPS URL | None | +| Key lifecycle | Generated at startup, published via JWKS | Generated in keystore at enrollment, loaded by handle | +| Token acquisition | Self-signed at startup | AP refresh endpoint (automatic via SDK) | +| Metadata | Publishes `/.well-known/aauth-agent.json` | AP publishes it | +| Code entry point | `MapAAuthAgentWellKnown()` + `AgentTokenBuilder` | `AAuthClientBuilder.Bootstrap().EnrolAsync()` | + ## Self-Issued Agent Tokens (Hosted Services) 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. @@ -270,8 +544,10 @@ using var client = new HttpClient(pipeline); ## Next Steps - [Signing Modes Overview](signing-modes/overview.md) — choose the right mode for your use case -- [Identity-Based Access](workflows/identity-based-access.md) — simplest workflow -- [PS-Asserted Access](workflows/ps-asserted-access.md) — full authorization flow +- [Identity-Based Access](workflows/identity-based-access.md) — simplest workflow (no PS needed) +- [PS-Asserted Access](workflows/ps-asserted-access.md) — full three-party authorization flow +- [Bootstrap & Enrollment](workflows/bootstrap-enrollment.md) — detailed AP enrollment for CLI/desktop agents +- [Server Guide](server/verification-middleware.md) — verification middleware and token issuance - [Protocol Concepts](concepts.md) — understand the full picture ## Protocol Reference From 1c843de49a29e241b65c22a2436bdba11e1d15be Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Thu, 28 May 2026 09:06:37 +0000 Subject: [PATCH 2/5] docs: address PR review comments - Add WebApplication setup to README agent example (missing app context) - Add UseAAuthChallenge middleware to resource examples (issues 401 + resource_token when agent lacks auth token) - Fix AAuth-Requirement header format: resource-token (hyphen) per spec - Fix resource token claim name: agent_jkt (not jkt) - Fix auth token PoP claim: cnf.jwk (not cnf.jkt) per spec - Fix SDK type reference: InteractionHandlingOptions (not InteractionWaitMode) - Fix Bootstrap call shape: Bootstrap(url, agentId) takes arguments - Add AAuth.Crypto using to README resource example --- README.md | 19 +++++++++++++++---- docs/getting-started.md | 22 +++++++++++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 11d2c9f..08ac48a 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,13 @@ using AAuth.HttpSig; using AAuth.Server; using AAuth.Tokens; +var builder = WebApplication.CreateBuilder(args); var key = AAuthKey.Generate(); const string Kid = "svc-key-1"; var issuer = "https://my-service.example"; +var app = builder.Build(); + // Publish agent metadata so resources can discover the JWKS app.MapAAuthAgentWellKnown(new AAuthAgentMetadataOptions { @@ -103,12 +106,14 @@ using var client = new AAuthClientBuilder(key) #### Resource (Server-Side) -The resource verifies signatures, publishes metadata, and issues resource tokens as challenges: +The resource verifies signatures, publishes metadata, and issues resource token challenges: ```csharp +using AAuth.Crypto; using AAuth.DependencyInjection; using AAuth.Server; +var builder = WebApplication.CreateBuilder(args); var resourceKey = AAuthKey.Generate(); // Register AAuth resource services @@ -129,13 +134,19 @@ app.UseAAuthVerification(new AAuthVerificationOptions { ResourceIdentifier = "https://resource.example", RequireIssuerVerification = true, - // Trust specific PSes — the resource verifies auth tokens against their JWKS. - // Omit to accept any PS (claims are namespaced by issuer per spec). TrustedAuthTokenIssuers = new HashSet { "https://ps.example" }, }); + +// Issue 401 + resource_token when agent presents only an agent token +app.UseAAuthChallenge(new ChallengeOptions +{ + ResourceSigningKey = resourceKey, + ResourceKeyId = "resource-key-1", + ResourceIdentifier = "https://resource.example", +}); ``` -When an agent presents an `auth_token`, the resource verifies its signature against the PS's published JWKS (discovered at `{iss}/.well-known/aauth-person.json`). The `TrustedAuthTokenIssuers` allow-list restricts which Person Servers the resource will accept auth tokens from. +The `UseAAuthChallenge` middleware (registered after verification) automatically returns `401` with an `AAuth-Requirement` header containing a resource token when the agent lacks an auth token. The `TrustedAuthTokenIssuers` allow-list restricts which Person Servers the resource will accept auth tokens from. #### Agent Calls the Resource diff --git a/docs/getting-started.md b/docs/getting-started.md index 1941d14..d59720e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -172,10 +172,10 @@ The resource verifies the HTTP signature, extracts the `ps` claim from the agent ``` HTTP/1.1 401 Unauthorized -AAuth-Requirement: requirement=auth-token;resource_token="" +AAuth-Requirement: requirement=auth-token; resource-token="" ``` -The resource token contains: issuer (resource URL), audience (PS URL), agent identifier, agent key thumbprint (`jkt`), and requested scope. +The resource token contains: issuer (resource URL), audience (PS URL), agent identifier, agent key thumbprint (`agent_jkt`), and requested scope. **3. Agent → Person Server (token exchange)** @@ -201,7 +201,7 @@ The PS: **5. Consent: immediate vs deferred** - **Immediate**: User is online and grants consent in real time. PS returns the auth token directly. -- **Deferred**: User is not available. PS returns `202 Accepted` with `requirement=interaction` and a `pending` URL. The agent polls until the user consents (SDK handles this via `InteractionWaitMode`). +- **Deferred**: User is not available. PS returns `202 Accepted` with `requirement=interaction` and a `pending` URL. The agent polls until the user consents (SDK handles this via `InteractionHandlingOptions`). **6. Person Server → Agent (auth token)** @@ -209,7 +209,7 @@ The PS issues an `auth_token` (`aa-auth+jwt`) containing: - `iss`: PS URL - `aud`: Resource URL - `sub`: User identifier (stable, PS-scoped) -- `cnf.jkt`: Agent's key thumbprint (proof-of-possession binding) +- `cnf.jwk`: Agent's public key (proof-of-possession binding) - `scope`: Granted scope - Optional identity claims: `email`, `tenant`, `groups`, `roles` @@ -228,7 +228,7 @@ Signature: sig=:: The resource verifies the auth token: - Fetches the PS's JWKS (from `{iss}/.well-known/aauth-person.json`) and verifies the JWT signature - Checks `aud` matches its own identifier -- Confirms `cnf.jkt` matches the signing key on the HTTP request (proof-of-possession) +- Confirms `cnf.jwk` matches the key used to sign the HTTP request (proof-of-possession) - Evaluates the granted `scope` against the requested operation - Optionally checks the issuer is in `TrustedAuthTokenIssuers` @@ -316,7 +316,15 @@ app.UseAAuthVerification(new AAuthVerificationOptions TrustedAuthTokenIssuers = new HashSet { "https://ps.example" }, }); -// Protected endpoint — if agent lacks auth, middleware issues 401 + resource_token +// Issue 401 + resource_token when agent presents only an agent token +app.UseAAuthChallenge(new ChallengeOptions +{ + ResourceSigningKey = resourceKey, + ResourceKeyId = "resource-key-1", + ResourceIdentifier = "https://resource.example", +}); + +// Protected endpoint — reached only after auth token is verified app.MapGet("/data", (HttpContext ctx) => { var agent = ctx.GetAAuthAgent(); // parsed from verified signature @@ -347,7 +355,7 @@ The `ChallengeHandler` intercepts the `401`, extracts the resource token, exchan | Key lifecycle | Generated at startup, published via JWKS | Generated in keystore at enrollment, loaded by handle | | Token acquisition | Self-signed at startup | AP refresh endpoint (automatic via SDK) | | Metadata | Publishes `/.well-known/aauth-agent.json` | AP publishes it | -| Code entry point | `MapAAuthAgentWellKnown()` + `AgentTokenBuilder` | `AAuthClientBuilder.Bootstrap().EnrolAsync()` | +| Code entry point | `MapAAuthAgentWellKnown()` + `AgentTokenBuilder` | `AAuthClientBuilder.Bootstrap(url, agentId).EnrolAsync()` | ## Self-Issued Agent Tokens (Hosted Services) From 2df1da2357fdb752649411616d1ac0defe2678f5 Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Thu, 28 May 2026 11:14:29 +0000 Subject: [PATCH 3/5] docs: clarify Resource-Managed signing mode options in flows table --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index d59720e..f2977c0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -118,7 +118,7 @@ AAuth supports four resource access modes. Each adds parties and capabilities: | Flow | Parties | When to Use | Signing Mode | |------|---------|-------------|--------------| | **Identity-Based** | Agent + Resource | API-key replacement, simple access control by identity | `hwk` or `jwks_uri` | -| **Resource-Managed** (two-party) | Agent + Resource | Resource handles its own auth (interaction, existing OAuth) | Any | +| **Resource-Managed** (two-party) | Agent + Resource | Resource handles its own auth (interaction, existing OAuth) | Any (`hwk`, `jwks_uri`, or `jwt`) | | **PS-Asserted** (three-party) | Agent + Resource + PS | User consent required, resource delegates auth to PS | `jwt` | | **Federated** (four-party) | Agent + Resource + PS + AS | Cross-domain policy, resource has its own Access Server | `jwt` | From 9e7e128c3518f768c4ac3c42a4e51cf0c06e22a2 Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Thu, 28 May 2026 11:16:01 +0000 Subject: [PATCH 4/5] docs: add context to Quick Start HWK example explaining pseudonymous mode --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 08ac48a..ca80ece 100644 --- a/README.md +++ b/README.md @@ -31,18 +31,21 @@ The SDK supports all four signing modes (`hwk`, `jwks_uri`, `jwt`, `jkt-jwt`), t dotnet add package AAuth --prerelease ``` +The simplest mode is **pseudonymous (HWK)** — the agent signs every request with an inline public key. No Agent Provider, no Person Server, no registration. The resource sees a stable key thumbprint it can use for rate-limiting or access control, but doesn't know the agent's identity. + ```csharp using AAuth.Crypto; using AAuth.HttpSig; -var key = AAuthKey.Generate(); +var key = AAuthKey.Generate(); // Ed25519 keypair using var client = new AAuthClientBuilder(key) - .UseHwk() + .UseHwk() // Pseudonymous mode: inline public key in Signature-Key header .Build(); var response = await client.GetAsync("https://resource.example/data"); -// Every request is signed per RFC 9421 — no bearer tokens +// Request is signed per RFC 9421 — the resource verifies the signature +// using the public key from the Signature-Key: sig=hwk;jkt="...";jwk="..." header ``` ### Three-Party Flow (Agent → Resource → Person Server) From f781318ba973b2998137b51041dd307ddde8dbc7 Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Thu, 28 May 2026 11:20:05 +0000 Subject: [PATCH 5/5] docs: expand signing modes valid combinations table to all access modes Add Resource-Managed and Federated access modes to the table. Clarify the distinction between signing modes (how the agent proves identity) and access modes (how authorization is decided). Add note explaining why Identity-Based access supports hwk despite hwk being pseudonymous. --- docs/signing-modes/overview.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/signing-modes/overview.md b/docs/signing-modes/overview.md index be84eac..32d2221 100644 --- a/docs/signing-modes/overview.md +++ b/docs/signing-modes/overview.md @@ -69,13 +69,23 @@ var handler = new AAuthSigningHandler(key, provider); | Remote key discovery (JWKS) | — | — | ✓ | — | | Person Server binding | — | — | — | ✓ | -## Valid Combinations per Flow +## Valid Combinations per Access Mode -| Flow | Valid Modes | Rationale | -|------|-----------|-----------| -| Identity-based (no PS) | `hwk`, `jwks_uri` | No PS-issued token available | -| Three-party (with PS) | `jwt` only | Spec: agent MUST present agent token via `scheme=jwt` | -| Bootstrap key rotation | `jkt-jwt` | Two-key delegation from durable to ephemeral | +Signing modes and access modes are orthogonal concepts: +- **Signing mode** = what appears in `Signature-Key` (how the agent proves identity) +- **Access mode** = how the resource decides authorization (who grants access) + +The access mode determines which signing modes are valid: + +| Access Mode | Valid Signing Modes | Why | +|-------------|--------------------:|-----| +| **Identity-Based** | `hwk`, `jwks_uri` | Resource decides from the signature alone. `hwk` gives pseudonymous access (key thumbprint only); `jwks_uri` gives named agent identity. No PS involvement, so `jwt` is not applicable. | +| **Resource-Managed** (two-party) | `hwk`, `jwks_uri`, `jwt` | Resource handles its own authorization (interaction, internal policy). Any signing mode works because the resource doesn't issue resource tokens to a PS — it manages access itself. | +| **PS-Asserted** (three-party) | `jwt` only | The resource issues a `resource_token` with `aud=PS`. The PS must verify the agent's identity via the agent token (`aa-agent+jwt`), which requires `scheme=jwt` in `Signature-Key`. | +| **Federated** (four-party) | `jwt` only | Same as PS-Asserted — the PS federates with the AS, but the agent-side requirement is identical: present the agent token via `scheme=jwt`. | +| **Bootstrap key rotation** | `jkt-jwt` | Special case: an ephemeral key is bound to a durable identity via a naming JWT. Used during key rotation, not as a primary access mode. | + +> **Common confusion**: "Identity-Based" access mode supports the `hwk` (pseudonymous) signing mode even though `hwk` doesn't disclose a named identity. The term "Identity-Based" refers to the *access pattern* — the resource grants or denies based solely on the cryptographic signature, with no token exchange. The resource may allowlist specific key thumbprints (pseudonymous) or specific agent identifiers (`jwks_uri`). Both are "identity-based" in the sense that no PS or AS is involved. ## Anatomy of a Signed Request