WhoAmI flow isolation + scope/role authorization#29
Merged
Conversation
- SDK: roles/groups claims, scope handler requires Authorized, AddAAuthRolePolicy - WhoAmI: isolated per-mode pipelines + /jwt, /jwt/admin, /jwt/roles - MockPersonServer: requested-scope echo + role/group assertion - Consumers: point three-party calls at /jwt - Tests: WhoAmIFlowTests updated for new flows - Plan docs under .agent/plans
- consent.ts: grantConsent accepts optional scope param (per-hop keying) - SampleApp call-chain.spec.ts: grant hop-1 with 'orchestrate' scope - GuidedTour TourSession.cs: seed hop-1 consent with 'orchestrate' scope - research.md: record AgentConsole permutation validation (all 6 pass); document pre-existing AP in-memory-enrollment vs disk-cache quirk
Phase 7 of whoami-flow-isolation-scopes plan: - GuidedTour: README/UI text + step titles -> per-mode endpoints and /jwt - SampleApp: Jwt/Deferred snippet narrative -> /jwt + scope/scheme fields - AgentConsole: new README (signing-mode->path map, validated invocations, admin/roles endpoints, consent curls, enrollment-cache quirk) - samples/README.md: WhoAmI 7-endpoint contract + updated console examples - docs/**: AddAAuthRolePolicy, scope-requires-Authorized, roles/groups claims, isolated per-mode pipelines, per-endpoint scope challenges, /jwt references
…sts) Apply maintainer-directed remediations from the Phase 8 self-review: - M1: MockPersonServer asserts whoami-admin role/groups only for recognized admin demo agents (id starts with aauth:demo@), so role-based denial is exercised instead of every auth token carrying the role. - M2: add AAuthScopeHandlerTests (Authorized+scope succeeds; wrong scope, non-Authorized level with stray scope, and missing result all denied) and WhoAmIFlowTests.RoleFlow_Returns403_WhenAgentLacksRole. - M3: document that /jwt/roles challenges only for the whoami scope and yields an unrecoverable 403 if the PS withholds the role (G7 step-up out of scope), in WhoAmI Program.cs and docs/server/authorization-policies.md. unit 333 + conformance 345 + e2e 20/20 pass.
…/authZ docs Phase 9 — namespace PS-asserted identity by issuer and fail-closed trust: - AAuthAuthenticationHandler stamps Claim.Issuer = asserting PS on NameIdentifier/Role/Group and adds composite aauth:sub_iss (iss|sub) - AAuthVerificationMiddleware rejects auth tokens whose iss is not in TrustedAuthTokenIssuers (null/empty = reject all) Phase 10 — PS-side resource-token verification (spec G9): - TokenVerifier.VerifyResourceTokenAsync runs the 7 recipient checks via JWKS discovery (typ, dwk+sig, exp/iat, aud, agent, agent_jkt, approver) - MockPersonServer /token verifies the posted resource_token and maps failures to invalid_resource_token / expired_resource_token (401) - Tests: +7 TokenVerifier resource-token cases; +forged/+tampered PS cases; WhoAmIFlowTests negative test rewired to mutually-wired PS/WhoAmI pair Phase 11 — docs: - New docs/server/authn-authz.md covering the authN/authZ pipeline and minimal-API and classic-MVC wiring (cross-linked from index + related docs) - token-issuance verify section, configuration keys, README/workflow fixes
Make the SampleApp call-chain demo (Agent → Orchestrator → WhoAmI)
demonstrate the spec's §Interaction Chaining end-to-end with two real
human-consent hops, replacing the broken "no onInteractionRequired
callback" deferral path.
SDK:
- Add AAuthInteractionChainedException (carries the AAuthInteraction).
An intermediary's OnInteractionRequired throws it to abort the in-flight
exchange before DeferredPoller blocks; the endpoint catches it and
re-emits its own 202. The existing try/finally (no catch) propagates it
cleanly. +3 unit tests in InteractionChainingTests.
Orchestrator:
- New in-memory PendingStore (id → upstream token + PS url/code).
- RunChainAsync builds the downstream client with WithCallChaining +
WithChallengeHandling whose callback throws the chained exception and
declares no "interaction" capability (no user to relay to).
- GET / catches the chained exception, parks a pending entry, and
re-emits its OWN 202 (own Location, pass-through PS url/code).
- GET /pending/{id} re-drives the hop with the stored upstream token:
202 (still pending) / 200 (consent) / 403 (denied). Hop-2 auto-grant
removed.
SampleApp (CallChain.razor):
- Wire BOTH WithChallengeHandling (hop-1 PS 202) and WithInteractionHandling
(hop-2 chained resource 202) into a shared SurfaceInteraction handler;
ChallengeHandler only acts on 401, so the chained 202 needs the
interaction pipeline.
- Make the two-approval flow explicit: upfront warning + per-hop colored
"Approval N of 2" badges (blue hop 1 / teal hop 2) and hop-specific copy.
- Page no longer resets consent; consent state is controlled externally so
the pre-grant happy-path spec stays green.
- Fix the rendered code snippets to match the real implementation (two
agent callbacks; full Orchestrator RunChainAsync + endpoint).
Tests + docs:
- New call-chain-deferred.spec.ts drives both consent popups to a final
nested-act 200, plus a hop-1 deny path. Existing call-chain.spec.ts
(pre-grant) stays green.
- Rewrite docs/advanced/interaction-chaining.md with the real
abort/re-emit pattern and agent-side WithInteractionHandling; cross-link
call-chaining.md and deferred-consent.md.
There was a problem hiding this comment.
Pull request overview
This PR refactors the WhoAmI sample so each AAuth access mode is an isolated, self-contained middleware pipeline, and extends the SDK + samples to demonstrate scope-based and role-based authorization (plus interaction chaining across a multi-hop call-chain demo). It also tightens verification/authorization semantics (notably: scope enforcement requires Authorized level; PS-issued auth tokens are accepted only from explicitly trusted issuers; and MockPersonServer now verifies resource tokens via JWKS discovery).
Changes:
- SDK: add roles/groups claims support end-to-end, add
AddAAuthRolePolicy, tighten scope handler semantics, addVerifyResourceTokenAsync, and implement interaction-chaining abort viaAAuthInteractionChainedException. - Samples: WhoAmI split into
/hwk,/jkt-jwt,/jwks-uri,/jwt,/jwt/admin,/jwt/roles(+ unauthenticated index), plus Orchestrator pending-store based chained-202 re-emit and SampleApp UI wiring for two-hop consent. - Tests/docs: add/extend unit + integration + conformance + Playwright coverage and update documentation to match the new flows and authN/authZ wiring.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/helpers/consent.ts | Extend consent helper to optionally include scope. |
| tests/AAuth.Tests/Tokens/TokenVerifierTests.cs | Add unit tests for VerifyResourceTokenAsync (happy + failure cases). |
| tests/AAuth.Tests/Server/AAuthScopeHandlerTests.cs | New unit tests for scope policy requiring Authorized level + exact scope membership. |
| tests/AAuth.Tests/Integration/WhoAmIFlowTests.cs | Update integration coverage to new WhoAmI endpoints, trusted-issuer behavior, roles flow, and scope in resource token. |
| tests/AAuth.Tests/Integration/MockPersonServerTests.cs | Add in-process resource discovery stub and tests for rejecting forged/tampered resource tokens. |
| tests/AAuth.Tests/Agent/InteractionChainingTests.cs | New unit tests for aborting deferred polling via AAuthInteractionChainedException. |
| tests/AAuth.Conformance/Observability/ActivityDiagnosticsTests.cs | Set TrustedAuthTokenIssuers in conformance harness options. |
| tests/AAuth.Conformance/HttpSignatures/VerificationMiddlewareTests.cs | Set TrustedAuthTokenIssuers in verification middleware conformance tests + add fail-closed test. |
| tests/AAuth.Conformance/HttpSignatures/ChallengeMiddlewareTests.cs | Set TrustedAuthTokenIssuers in challenge middleware conformance host. |
| tests/AAuth.Conformance/HttpSignatures/AuthorizationIntegrationTests.cs | Set trusted issuers; assert (iss, sub) namespacing surfaced via claims. |
| tests/AAuth.Conformance/CallChaining/UseAAuthIntermediaryTests.cs | Configure trusted issuers for intermediary verification. |
| src/AAuth/Tokens/TokenVerifier.cs | Add VerifyResourceTokenAsync implementing resource-token verification via issuer metadata/JWKS. |
| src/AAuth/Tokens/AuthTokenBuilder.cs | Add Roles/Groups and emit RFC 9068 JSON string arrays when present. |
| src/AAuth/Server/AAuthVerificationResult.cs | Add Roles/Groups to verification result feature. |
| src/AAuth/Server/AAuthVerificationOptions.cs | Document fail-closed behavior for trusted auth-token issuers. |
| src/AAuth/Server/AAuthVerificationMiddleware.cs | Parse roles/groups; enforce fail-closed trusted-issuer allowlist for auth tokens. |
| src/AAuth/Server/AAuthScopeHandler.cs | Require Level == Authorized in addition to scope membership. |
| src/AAuth/Server/AAuthAuthenticationHandler.cs | Map roles/groups into claims; namespace identity claims by asserting issuer. |
| src/AAuth/DependencyInjection/AAuthResourceServiceCollectionExtensions.cs | Add AddAAuthRolePolicy helper. |
| src/AAuth/Agent/AAuthInteractionExceptions.cs | Add AAuthInteractionChainedException for interaction chaining abort/re-emit pattern. |
| samples/WhoAmI/Program.cs | Split into per-mode verification/challenge branches; add scope + role policies; add flow index; remove identity fallback. |
| samples/SampleApp/playwright-tests/call-chain.spec.ts | Pre-grant correct hop-1 scope consent for orchestrator. |
| samples/SampleApp/playwright-tests/call-chain-deferred.spec.ts | New E2E spec for two-hop deferred consent + chained interaction flow. |
| samples/SampleApp/Components/Pages/Jwt.razor | Update rendered snippets/requests to /jwt path and correct keystore usage. |
| samples/SampleApp/Components/Pages/Deferred.razor | Update rendered snippets/requests to /jwt path. |
| samples/SampleApp/Components/Pages/CallChain.razor | Wire both challenge-handling and interaction-handling callbacks; UI updates for two approvals. |
| samples/README.md | Update WhoAmI sample description and documented endpoints. |
| samples/Orchestrator/Program.cs | Add interaction chaining: pending store + re-emit 202 + poll endpoint; enforce trusted PS issuer. |
| samples/Orchestrator/PendingStore.cs | New in-memory pending store for chained interaction flow. |
| samples/MockPersonServer/README.md | Update docs: PS now verifies resource tokens using SDK helper. |
| samples/MockPersonServer/Program.cs | Verify resource tokens via VerifyResourceTokenAsync; echo requested scope; conditionally assert roles/groups; scope-keyed consent. |
| samples/GuidedTour/TourSession.cs | Update tour steps/paths; seed orchestrate scope consent for call-chain mode. |
| samples/GuidedTour/README.md | Update documentation for new WhoAmI endpoint layout and /jwt three-party path. |
| samples/GuidedTour/playwright-tests/identity.spec.ts | Update step text assertions for new identity flow wording. |
| samples/GuidedTour/playwright-tests/deferred.spec.ts | Update step text assertion to /jwt. |
| samples/GuidedTour/playwright-tests/autonomous.spec.ts | Update step text assertion to /jwt. |
| samples/GuidedTour/Components/Pages/Tour.razor | Update identity flow explanation to refer to per-mode endpoints. |
| samples/AgentConsole/README.md | New README documenting behavior, mode→path mapping, and consent instructions. |
| samples/AgentConsole/Program.cs | Update default jwt path mapping from / to /jwt. |
| docs/workflows/ps-asserted-access.md | Document PS resource-token verification in the workflow. |
| docs/workflows/deferred-consent.md | Add note about PS resource-token verification and link interaction chaining. |
| docs/workflows/call-chaining.md | Update WhoAmI downstream path to /jwt and link interaction chaining docs. |
| docs/server/verification-middleware.md | Document fail-closed trusted-issuer allowlist and roles/groups surfacing. |
| docs/server/token-issuance.md | Add section documenting PS-side resource-token verification API + checks. |
| docs/server/challenge-middleware.md | Add per-endpoint scope challenge branching guidance. |
| docs/server/authorization-policies.md | Expand docs to cover scope+role policies, level semantics, and namespacing. |
| docs/server/authn-authz.md | New doc describing full authN/authZ pipeline and wiring for minimal APIs + MVC. |
| docs/reference/dependency-injection.md | Document authN/authZ registrations and new role policy helper. |
| docs/reference/configuration.md | Document fail-closed trusted-issuer behavior and sample config keys. |
| docs/README.md | Link to new authN/authZ doc. |
| docs/advanced/interaction-chaining.md | Rewrite with abort/re-emit pattern using AAuthInteractionChainedException. |
| .agent/plans/2026-06-01-whoami-flow-isolation-scopes/research.md | New research doc for the initiative. |
| .agent/plans/2026-06-01-whoami-flow-isolation-scopes/implementation-plan.md | New phased implementation plan with DoD checklists. |
Copilot's findings
- Files reviewed: 53/53 changed files
- Comments generated: 8
Add the research and implementation plan documents for the call-chain interaction-chaining work (Phases 2-5) that were omitted from the initial feature commit.
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
Refactors
samples/WhoAmIso each AAuth resource access mode is an independent, self-contained pipeline, and adds a first-class demonstration of scope- and role-based authorization built on the SDK's ASP.NET Core authn/authz integration. It also adds genuine Interaction Chaining to the call-chain demo so the multi-hop flow surfaces real human consent at each hop. Spec compliance is the golden rule; backward compatibility with prior paths/response shapes is intentionally waived (alpha).Motivation
Previously WhoAmI multiplexed all signing/access modes through one handler with in-line branching and an identity-based fallback (an agent token with no
psreturned200). That obscured how the four AAuth access modes differ and let a proof-of-possession-only token reach protected claims. This PR isolates each flow and makes the authorization model explicit and enforceable.Separately, the interactive call-chain demo could not actually defer: when a hop lacked standing consent the SDK threw because no
onInteractionRequiredcallback was wired, and the intermediary had no way to propagate the requirement to the user. This PR implements the spec's §Interaction Chaining so both hops surface real consent.What changed
SDK (
src/AAuth)AuthTokenBuilderemitsroles/groups(RFC 9068 JSON string arrays) when set; omits them when null.AAuthVerificationResultexposesRoles/Groups;AAuthVerificationMiddlewareparses them.AAuthAuthenticationHandlermaps roles toClaimTypes.Roleand groups to a newaauth:groupclaim.AAuthScopeHandlernow requiresLevel == Authorizedand scope membership — closing the PoP-only bypass.AddAAuthRolePolicy(name, role)DI helper: requires Authorized level + the role.AAuthInteractionChainedException(carries theAAuthInteraction). An intermediary'sOnInteractionRequiredthrows it to abort the in-flight token exchange beforeDeferredPollerblocks; the endpoint catches it and re-emits its own202. The existingtry/finally(nocatch) propagates it cleanly — no other SDK change required.WhoAmI sample — isolated pipelines
Each mode is its own
UseWhenbranch with no shared catch-all:/hwk,/jkt-jwt,/jwks-uri— signature-only / identity./jwt— three-party baseline, scopewhoami./jwt/admin— three-party, scopewhoami:admin./jwt/roles— three-party RBAC, rolewhoami-admin./— unauthenticated flow index.The identity-based fallback was removed; a PoP-only token now gets a proper
401challenge instead of a200.Interaction chaining — call-chain demo (Agent → Orchestrator → WhoAmI)
PendingStore(id → upstream token + PSurl/code).RunChainAsyncbuilds the downstream client withWithCallChaining+ aWithChallengeHandlingcallback that throwsAAuthInteractionChainedExceptionand declares nointeractioncapability (it has no user to relay to).GET /catches the chained exception, parks a pending entry, and re-emits its own202(ownLocation, pass-through PSurl/code).GET /pending/{id}re-drives the hop with the stored upstream token →202(pending) /200(consent) /403(denied). The hop-2 auto-grant shortcut is removed.CallChain.razor): wires bothWithChallengeHandling(hop-1 PS202) andWithInteractionHandling(hop-2 chained resource202) into one shared handler —ChallengeHandleronly acts on401, so the chained202needs the interaction pipeline. The two-approval flow is now explicit: an upfront warning plus per-hop colored "Approval N of 2" badges (blue hop 1 / teal hop 2). The page no longer resets consent, so the pre-grant happy-path spec stays green. Rendered code snippets corrected to match the real implementation.Consumers
MockPersonServerechoes the requested scope into the auth token, keys consent by(agent, resource, scope), publisheswhoami:admin, and asserts the demo role/groups only for admin demo agents (so role denial is exercised).AgentConsole,Orchestrator,SampleApp,GuidedTourupdated to target/jwtand the per-mode endpoints.Self-review remediations (Implementation Validator — "sound to merge, no blockers")
AAuthScopeHandlerTestsand a/jwt/roles403 denial integration test./jwt/roleschallenges only forwhoamiand yields an unrecoverable 403 if the PS withholds the role (G7 step-up is out of scope).Docs
docs/advanced/interaction-chaining.mdwith the real abort/re-emit pattern (AAuthInteractionChainedException+ endpoint catch) and the agent-sideWithInteractionHandlingwiring; cross-linkeddocs/workflows/call-chaining.mdanddocs/workflows/deferred-consent.md.docs/server/authn-authz.mdcovering the authN/authZ pipeline and minimal-API / classic-MVC wiring.Testing
call-chain-deferred.spec.tsthat drives both consent popups to a final nested-act200plus a hop-1 deny path; the existing pre-grantcall-chain.spec.tsstays green.200Out of scope
Multi-scope
AllOf/AnyOf(G6), insufficient-scope/role step-up re-challenge (G7), four-party AS pipeline, production-grade pending persistence/GC on the Orchestrator,payment/clarificationcapabilities, and live-infra redeploy.Automated review triage (copilot-pull-request-reviewer)
All four inline comments were validated against the actual code path and the AAuth spec; none required a code change (3 false positives, 1 contradicted by a spec MUST). Threads replied-to and resolved.
AAuthAuthenticationHandler.csnullassertingIssuer(×3) — false positive. The BCLClaim(type, value, valueType, issuer)ctor does not throw on a null/empty issuer; it defaults toClaimsIdentity.DefaultIssuer(runtime-verified on net10.0). Signature-only schemes (hwk/jkt) correctly attribute claims to the local authority — noArgumentNullExceptionpossible.WhoAmI/Program.cstrusted-issuer matching — invalid; contradicted by spec. §Identifiers mandates "Implementations MUST perform exact string comparison on server identifiers" (lowercase, no trailing slash). The current ordinalHashSet.Contains(iss)is exactly compliant; normalizing would accept spec-invalid identifiers and weaken the fail-closed allow-list.