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..ca80ece 100644 --- a/README.md +++ b/README.md @@ -31,54 +31,155 @@ 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) -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 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 +{ + 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(); +``` + +#### Resource (Server-Side) -var response = await client.GetAsync("https://resource.example/protected"); +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 +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, + 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", +}); +``` + +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 + +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 ``` -See [Getting Started](docs/getting-started.md) for key persistence, DI integration, and all signing modes. +**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..f2977c0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -75,6 +75,288 @@ 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 (`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` | + +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 (`agent_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 `InteractionHandlingOptions`). + +**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.jwk`: Agent's public key (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.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` + +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" }, +}); + +// 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 + 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(url, agentId).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 +552,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 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