feat: four-party federated access with stub and Keycloak Access Servers#30
Merged
Conversation
Add the Phase 1 baseline for four-party (federated) AAuth access: a pure-SDK Access Server sample plus an isolated WhoAmI federated branch and a manual PS->AS exercise. No new public SDK API is required. - samples/MockAccessServer (port 5500): serves /.well-known/aauth-access.json + JWKS via MapAAuthAccessServerWellKnown; POST /token verifies the PS jwks_uri signature (with trusted-PS host pinning), verifies the body's agent_token and resource_token (aud=AS), and mints an aa-auth+jwt with dwk=aauth-access.json. Policy is a hard-coded allow stub this phase. - samples/WhoAmI: isolated /federated verification + challenge branch that emits a resource token with aud=AS (ChallengeOptions.PersonServerAudience) and trusts the AS issuer for the AS-issued auth token. - tests: MockAccessServerTests exercises the PS->AS POST /token flow, asserting the minted token shape and rejecting wrong-aud resource tokens and untrusted PS hosts. Full suite green (350 passed). - devcontainer: add docker-outside-of-docker for the upcoming Keycloak policy-engine phases. - .agent/plans: research + phased implementation plan for the work.
Add the Person Server's signed PS->AS token request client for the
four-party (federated) flow, plus its request/exception types and tests.
- AccessServerClient (AAuth.Tokens): discovers aauth-access.json, POSTs
the signed {resource_token, agent_token, upstream_token?} request with
an https-or-loopback + same-origin SSRF guard, handles 200 and the 202
deferred poll loop (reusing DeferredPoller), and runs the 7-step Auth
Token Delivery verification via the existing AuthTokenResponseValidator.
- Recognizes 402 Payment Required as AAuthPaymentRequiredException
(settlement is out of scope per the spec).
- Surfaces an unhandled 202 requirement=claims as NotSupportedException;
full identity-claims push is deferred to a dedicated later phase.
- AccessServerRequest groups the payload and delivery-verification context.
- 6 unit tests in AAuth.Tests cover success, upstream passthrough, 402,
requirement=claims, audience-mismatch delivery failure, and structured
token-endpoint errors. Full suite: 356 passed.
Plan: Phase 2 marked complete; adds a dedicated requirement=claims phase.
MockPersonServer now branches on the resource token's `aud`: when it names a trusted Access Server (not the PS itself) the PS forwards a signed PS->AS request via the SDK AccessServerClient and returns the AS-issued auth token, rather than minting one itself. When `aud` is the PS, the existing three-party (collapsed PS+AS) path is unchanged. - Peek `resource_token.aud` to route; gate federation on MockPersonServer:TrustedAccessServers (else 403 untrusted_access_server) - Verify the resource token's agent binding before forwarding; relay AS errors, 402 payment_required, and failed delivery verification as 502 - Register AccessServerClient over a named "aauth-federation" HttpClient so the PS->AS transport is testable in-process - Add MockPersonServerFederationTests (federate, untrusted AS, three-party) - Document three-party vs four-party in the sample README
Refactor the Mock AS allow-stub into a pluggable IAccessPolicy / AccessDecision seam so the AAuth crypto stays in the adapter while the allow/deny/needs- interaction decision is delegated. Selection is config-driven via AccessServer:PolicyProvider (default `stub`), mirroring the MockPersonServer:RequireConsent pattern; CI stays pure-.NET. - IAccessPolicy + AccessDecision (Allow/Deny/NeedsInteraction) + request DTO - StubAccessPolicy encodes the demo rule locally: any verified agent gets the base scope; an elevated scope (whoami:admin) needs the whoami-admin role - /token evaluates the policy and maps Deny->403 access_denied, an unreachable backend->503 (fail closed); NeedsInteraction placeholder pending Step B - Tests: grant elevated scope for admin agent; deny for non-admin (403) Part of Phase 4 (Keycloak as the policy engine); Keycloak provider next.
Wire the AS's Keycloak Policy Decision Point behind the IAccessPolicy seam
(Phase 4 Step B). The keycloak provider binds an interactive policy that
runs the OIDC authorization-code login plus a uma-ticket
response_mode=decision verdict, pushing PS-asserted claims via claim_token.
- KeycloakOptions: realm authority, client credentials, resource name
- KeycloakAccessPolicy: IAccessPolicy + IInteractiveAccessPolicy
(EvaluateAsync defers; BuildAuthorizationUrl + CompleteAsync drive login)
- AccessPendingStore: parks mint inputs across the browser round-trip
- /token emits 202 requirement=interaction (Location=/pending/{id})
- /interaction/login redirect, /interaction/callback completion,
/pending/{id} poll mirroring the PS deferred shape for DeferredPoller
- Browser-facing /interaction paths excluded from AAuth verification
- Fail closed when Keycloak is unreachable (503 policy_unavailable)
Tests: stub Keycloak HttpMessageHandler keeps CI pure-.NET; 4 new tests
cover the interactive grant, admin scope grant/deny, and the 202 emission.
…-party flow
The Person Server now federates to the Access Server in the background while
relaying the AS's interaction requirement (Keycloak login URL) back to the
agent. A FederatedPendingStore parks the in-flight federation; the agent polls
/federated-pending/{id} until the AS resolves the interactive login.
Add a Keycloak realm import, MockAccessServer keycloak settings, and Makefile targets (keycloak, access-server, demo-federated, agent-federated) so the four-party federated flow can be run manually end to end with Keycloak as the interactive policy engine.
The four-party Keycloak demo broke under docker-outside-of-docker: bind-mount paths resolved on the host (empty realm import dir -> aauth realm 404) and the published port was only reachable via the bridge gateway, not localhost. - Use the docker-in-docker feature (privileged) so bind mounts use dev-container paths and localhost:8080 works for both the AS backend and the forwarded host browser. - Forward the demo ports (8080 + 5000/5100/5301/5500). - Install make in the image (base dotnet/sdk image lacks it). - demo-federated builds the solution once, then runs services with --no-build to avoid a concurrent MSBuild file-lock race.
The MockAgentProvider keeps its agent registry in memory, so the AgentConsole's on-disk enrollment cache goes stale whenever the AP restarts, causing a 400 invalid_grant on token refresh. agent-federated now clears the cache first, and a standalone agent-reset target is available.
…arrows Add the federated (four-party) flow to the GuidedTour with a dedicated Access Server swimlane and rework the sequence-diagram arrow rendering. - Add Actor.AccessServer plus SubStep/SubStepsLabel to StepRecord so a step can render component-internal hops in a labeled box. - Add TourMode.Federated and StepFederated* steps (resource token aud=AS, PS->AS federation rendered as inner sub-steps, AS-minted aa-auth+jwt, replay, inspect); model parse-challenge as Agent->Agent self-steps to avoid duplicate right-to-left arrows. - Rewrite SequenceDiagram into request (solid) -> sub-steps box -> response (dotted, after the box) rows; response direction reverses the request; box title driven by SubStepsLabel. - Give the Access Server its own red swimlane lane/highlight; full-height swimlanes; dotted response arrows and labels with centered arrowheads. - Makefile: demo-federated now boots the Orchestrator and runs the PS with RequireConsent=true so deferred consent works under the federated target (parity with demo). - Update plan/research: Phase 6 delivered + findings; Phase 7 decisions.
Add MapAAuthAccessServer host helper, IAccessPolicy/IAccessPendingStore seams, and the AAuthClaimsResponse pairwise-subject + claims push model read from the request body. Pin trusted PS on pending GET/POST and bind the origin host; reject untrusted claims pushes.
Add SampleApp Federated page (nav entry + home card + shield-lock icon), GuidedTour Access Server entity highlighting, and the MockAccessServer host-helper migration onto the SDK endpoints. Wire the MockPersonServer claims push and add federated E2E specs.
Replace the demo-federated* targets with stub-default demo-tour / demo-sampleapp and explicit -keycloak variants, factor the duplicated Keycloak boot/env into reusable defines, and group targets into labelled sections. Update docs and plans to match the new target names.
Add a RequireConsent mode to the stub AccessPolicy so the federated flow returns 202 + an Access-Server-badged consent screen (Approve/Deny endpoints flip the pending entry), matching Keycloak's interactive UX. Force the Keycloak path to render its own consent screen via prompt=consent plus consentRequired on the realm client, and rename the client to "AAuth Access Server" so the consent prompt reads naturally. Badge the MockPersonServer consent pages as "Person Server" to disambiguate the two.
…consent Make the GuidedTour federated narrative and consent button text neutral between the stub AS and Keycloak (same 202 -> interaction URL -> poll -> mint; only the destination differs), add the call-chain two-hop deferred consent walkthrough, and update the SampleApp Federated page copy to reflect the AS-rendered consent screen.
…pecs Rewrite the federated specs to click Approve/Deny on the AS consent screen (popup badge assertion), add the call-chain two-hop deferred approval spec, bump the picker count to six flows, and run the stub AS with RequireConsent=true in the Playwright webServer.
Replace the per-app demo-tour/demo-sampleapp (and -keycloak) targets with a single demo (stub AS, both UIs) and demo-keycloak (live Keycloak, both UIs), each printing a grouped URL summary at the end. Update the README and workflow docs to match.
…parity - Fix PS-side example to call AccessServerClient.FederateAsync (was a non-existent ExchangeAsync) with the required request members. - Note that both the stub and Keycloak AS policies drive interactive consent, not Keycloak alone. - Document the stub AS RequireConsent screen in the Mock Access Server README. - Mark Phase 7-10 DoD items complete in the implementation plan.
- Add an Access Modes table (aligned with explorer.aauth.dev/access/compare) with a demo column to the repo README, and restructure it for progressive exposure: See It Run -> Quick Start (client) -> Building Servers. - Fix the three-party snippets to build their own self-issuing client with WithChallengeHandling instead of reusing the HWK client. - Add a dedicated package README (src/AAuth/README.md) with only absolute URLs so nuget.org renders correctly; point AAuth.csproj at it. - Getting Started: add a demos pointer, link/demo column for all four modes, a four-party section, de-duplicate the self-issuing block, and expand Next Steps.
There was a problem hiding this comment.
Pull request overview
Adds four-party federated access (Agent → Resource → Person Server → Access Server) across the SDK, samples, docs, and tests, including a pure-.NET stub Access Server and a Keycloak-backed policy option. This extends the existing three-party PS-asserted model by letting the resource delegate policy to an AS when the resource token’s aud points at an Access Server.
Changes:
- Introduces Access Server federation primitives in the SDK (request/response types, policy/pending-store abstractions, token builder extensions, and deferred polling hooks).
- Adds a new
MockAccessServersample (stub + Keycloak policy) and wires a new/federatedbranch into the WhoAmI resource plus new SampleApp/GuidedTour UI paths and Playwright specs. - Expands developer workflow support (Makefile demo targets, devcontainer Docker-in-Docker, documentation updates).
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/playwright.config.ts | Boots MockAccessServer for e2e runs and documents Keycloak gating. |
| tests/e2e/helpers/tour.ts | Adds Federated tour mode and step-count expectations. |
| tests/e2e/helpers/consent.ts | Adds Keycloak login helper for federated interaction popups. |
| tests/e2e/helpers/agents.ts | Adds Access Server base URL constant for e2e assertions. |
| tests/AAuth.Tests/Integration/MockPersonServerFederationTests.cs | Integration coverage for PS→AS federation behavior and relayed interaction. |
| tests/AAuth.Tests/Integration/MockAccessServerTests.cs | Integration coverage for stub Access Server token endpoint + claims push. |
| tests/AAuth.Tests/Integration/MockAccessServerKeycloakTests.cs | Integration coverage for Keycloak policy adapter (stubbed Keycloak in CI). |
| tests/AAuth.Tests/AccessServerClientTests.cs | Unit tests for AccessServerClient federation behaviors and requirement handling. |
| tests/AAuth.Tests/AAuth.Tests.csproj | Adds MockAccessServer project reference to test project. |
| src/AAuth/Tokens/AuthTokenBuilder.cs | Adds tenant claim support and AdditionalClaims merge with collision checks. |
| src/AAuth/Tokens/AccessServerRequest.cs | New request object for AccessServerClient.FederateAsync. |
| src/AAuth/Server/IAccessPolicy.cs | New Access Server policy seam (IAccessPolicy, decisions, interactive extension). |
| src/AAuth/Server/IAccessPendingStore.cs | New pending-decision store abstraction + in-memory implementation. |
| src/AAuth/README.md | Adds SDK README for NuGet packaging and documents access modes. |
| src/AAuth/Headers/AAuthClaimsResponse.cs | Typed claims-push response for §Claims Required callback. |
| src/AAuth/Headers/AAuthClaimsRequirement.cs | Typed projection for requirement=claims using required_claims body. |
| src/AAuth/Errors/AAuthPaymentRequiredException.cs | Typed exception for 402 Payment Required in federation. |
| src/AAuth/Agent/DeferredPoller.cs | Adds StopWhenAccepted hook for mid-poll requirement changes. |
| src/AAuth/AAuth.csproj | Packages src/AAuth/README.md into the NuGet README. |
| samples/WhoAmI/Program.cs | Adds /federated branch that trusts AS-issued auth tokens and challenges with aud=AS. |
| samples/SampleApp/playwright-tests/federated.spec.ts | E2e spec for federated flow via stub AS interactive consent. |
| samples/SampleApp/playwright-tests/federated-deferred.spec.ts | E2e spec for federated flow via Keycloak (gated by KEYCLOAK_E2E). |
| samples/SampleApp/Components/Pages/Home.razor | Adds Federated card; updates prerequisites command text. |
| samples/SampleApp/Components/Pages/Federated.razor | New Federated demo page showing four-party interaction + polling UX. |
| samples/SampleApp/Components/Layout/NavMenu.razor.css | Adds nav icon for federated page. |
| samples/SampleApp/Components/Layout/NavMenu.razor | Adds nav link for federated page. |
| samples/SampleApp/appsettings.json | Adds AccessServer URL configuration. |
| samples/README.md | Updates sample inventory and adds federated/Keycloak quickstart notes. |
| samples/MockPersonServer/README.md | Documents three-party vs four-party branching by resource_token.aud. |
| samples/MockPersonServer/FederatedPendingStore.cs | New store for PS-side federated pending state while AS resolves. |
| samples/MockAccessServer/README.md | New documentation for MockAccessServer behavior and configuration. |
| samples/MockAccessServer/Properties/launchSettings.json | Launch settings for MockAccessServer sample. |
| samples/MockAccessServer/Program.cs | New Access Server sample host wiring (MapAAuthAccessServer) + interaction endpoints. |
| samples/MockAccessServer/Policy/StubAccessPolicy.cs | Stub policy engine (claims requirement, role gate, optional consent). |
| samples/MockAccessServer/Policy/KeycloakOptions.cs | Keycloak adapter configuration options. |
| samples/MockAccessServer/Policy/KeycloakAccessPolicy.cs | Keycloak-backed interactive policy adapter with UMA need_info support. |
| samples/MockAccessServer/MockAccessServer.csproj | New ASP.NET Core project for the Access Server sample. |
| samples/MockAccessServer/keycloak/realm-aauth.json | Keycloak realm import for demo users/scopes/policies. |
| samples/MockAccessServer/appsettings.json | Default AS config (stub policy, Keycloak defaults). |
| samples/GuidedTour/wwwroot/app.css | Adds Access Server styling and response-arrow rendering improvements. |
| samples/GuidedTour/TourOptions.cs | Adds federated mode + AccessServerUrl option. |
| samples/GuidedTour/StepRecord.cs | Adds AccessServer actor, sub-step label, and response sub-step support. |
| samples/GuidedTour/README.md | Updates GuidedTour docs to include federated mode. |
| samples/GuidedTour/playwright-tests/picker.spec.ts | Updates picker spec for 6 flows. |
| samples/GuidedTour/playwright-tests/federated.spec.ts | New GuidedTour e2e spec for federated approve/deny (stub AS). |
| samples/GuidedTour/playwright-tests/call-chain.spec.ts | Updates call-chain e2e to reflect interactive approvals and new step count. |
| samples/GuidedTour/Components/StepList.razor | Adds Access Server actor display name. |
| samples/GuidedTour/Components/SequenceDiagram.razor | Adds dashed response arrows and sub-step labeling. |
| samples/GuidedTour/Components/Pages/Tour.razor | Adds Federated mode to UI and descriptive text updates. |
| samples/GuidedTour/Components/EntityHighlighter.cs | Adds Access Server highlighting rule. |
| samples/GuidedTour/appsettings.json | Adds default AccessServerUrl. |
| README.md | Expands top-level docs: access modes, demo commands, and updated quickstart structure. |
| Makefile | Adds MockAccessServer, Keycloak demo, demo consolidation, and agent helper targets. |
| docs/workflows/federated-access.md | Adds PS-side and AS-side code guidance + interaction/claims sections. |
| docs/workflows/call-chaining.md | Updates quickstart to use make demo. |
| docs/getting-started.md | Adds interactive “Try the flows” section and federated flow references. |
| AAuth.slnx | Adds MockAccessServer project to solution. |
| .devcontainer/Dockerfile | Installs make for running demo targets in container. |
| .devcontainer/devcontainer.json | Enables Docker-in-Docker and forwards Keycloak/AS ports. |
| .devcontainer/devcontainer-lock.json | Locks Docker-in-Docker feature version. |
| .agent/plans/2026-06-02-four-party-keycloak-as/phase-12-api-design.md | Adds delivered API design record for four-party work. |
Copilot's findings
- Files reviewed: 67/67 changed files
- Comments generated: 7
- src(access-server): evict in-memory pending entries after a 10-minute TTL so the demo IAccessPendingStore does not grow without bound; the TTL outlives the interactive round-trip and poll retries so a minted entry is not yanked from under a re-polling Person Server - samples(access-server): render the configured issuer authority in the consent banner instead of a hardcoded localhost:5500 - samples(sampleapp): correct prerequisites to `make demo` and list the stub Access Server + both UI ports - samples(guidedtour): reword the Federated step to reflect the default interactive consent at the Access Server (RequireConsent=true) - samples(readme): replace nonexistent demo-tour/demo-sampleapp targets with `make demo` / `make demo-keycloak` and list all started services
Follow-up to the C1 review fix in 0fa4b99 which changed the Home page prerequisites from `make demo-sampleapp` to `make demo`; the e2e assertion still expected the old text.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the four-party federated access flow (Agent → Resource → Person Server → Access Server) to the SDK and samples, with both a pure-.NET stub Access Server and a Keycloak-backed Access Server, plus interactive consent that "bubbles up" from the AS to the agent.
From the agent's perspective the flow is byte-for-byte identical to PS-asserted access — the PS↔AS federation (and any AS interaction) is transparent. The discriminator is the resource token's
aud: when it points at an Access Server, the PS federates instead of asserting access itself.What's included
SDK (
src/AAuth/)AccessServerClient.FederateAsync— signed PS→AS token exchange that drives the full AS lifecycle (202 → interaction/claims → poll → verifiedaa-auth+jwt).MapAAuthAccessServerhost helper:/.well-known/aauth-access.json+ JWKS, RFC 9421 request-signature verification (pinned to trusted Person Servers), token verification, pluggableIAccessPolicy, andIAccessPendingStorefor deferred decisions.IAccessPolicy(Allow/Deny/NeedsInteraction/NeedsClaims/NeedsPayment), claims-push API (requirement=claims), anddwk=aauth-access.jsonminting.Samples
MockAccessServerwith config-selectedstubandkeycloakpolicies; the stub renders its own interactive Approve/Deny consent screen (AccessServer:RequireConsent).SampleAppand a Federated flow inGuidedTour(provider-neutral labels — stub and Keycloak look identical to the agent).make demo(stub AS, no Docker) andmake demo-keycloak(live Keycloak), each launching both UIs and printing a grouped URL summary.Tests
KEYCLOAK_E2E=1).Spec compliance
Reviewed against
aauth-spec/draft-hardt-oauth-aauth-protocol.md— compliant: auth token structure (aa-auth+jwt, required claims, ≤1h lifetime, nestedact), signed PS→AS federation, 202 requirement grammar (RFC 8941), claims push, and auth-token delivery verification. No spec violations found.Validation
make demoandmake demo-keycloakexercised end-to-end.