AAuth -11: require a person token at /authorize - #5
Merged
Conversation
The agent now presents an aa-person+jwt in Signature-Key at the
authorization endpoint instead of its agent token. A resource MUST have
verified a person token before it issues a resource token, so the
identity a resource token carries is PS-asserted rather than
agent-asserted.
- Verify the person token per §Person Token Verification: typ
aa-person+jwt, dwk aauth-person.json, JWKS at {iss}/.well-known/{dwk},
aud equal to this resource, cnf.jwk equal to the request signing key.
Challenge 401 AAuth-Requirement: requirement=person-token when absent
(no Signature-Key, or an agent token presented); 400
invalid_person_token when present but bad.
- Drop the `ps` body parameter. The person token's iss IS the person
server, so /authorize now takes only `scope`.
- Resource token drops the `agent` claim and gains `ps`, `sub`,
`person_token_jti`, `mission_s256` and `tenant`, all copied from the
verified person token. `agent_jkt` stays. exp is clamped to the person
token's. The PS checks ps, sub, mission_s256 and tenant against the
person token it issued and rejects on any mismatch or omission, so a
dropped tenant makes the token unredeemable for an org-affiliated
person.
- Every endpoint that reads a token from Signature-Key now states which
AAuth token types it accepts: verifySigJwt takes a required `accept`
set with no default, and enforces typ before anything else looks at
the token. /authorize accepts person, /api/demo accepts auth. A person
token and a PS-issued auth token share iss, dwk, aud, sub and cnf and
differ only in typ, so a verifier that omits that check takes a
credential carrying no authorization for one that carries it.
- alg is the fully-specified `Ed25519`, never the polymorphic `EdDSA`,
on both emit and accept: §Signature Algorithms allows no transition,
so JWT_ALG_PARAMS drops EdDSA rather than tolerating it. The alg strip
before crypto.subtle.importKey is unchanged and now carries a DO NOT
REMOVE note — workerd rejects an OKP JWK whose alg is "Ed25519".
- PS metadata field auth_token_endpoint, renamed from token_endpoint.
Browser narration — the flow gains a first hop and a second consent:
- The agent fetches a person token from the PS's person_token_endpoint
before whoami and before notes /authorize, then presents it in place
of the agent token.
- That endpoint answers 202 whenever the person has not used the
resource before, which is every first run of the demo. The polling
loop is now token-agnostic: startDeferredPolling takes `tokenField`
(person_token | auth_token) and `copyPrefix`, and one runDeferredResponse
renders the 202 for both legs — long-poll step, consent card,
persisted pending record.
- The pending record gains `stage`, so a same-tab redirect to the PS
resumes into the right leg. The person-token leg re-enters the
ceremony at continueWhoami / continueNotesAuthorize, which run the
resource call and the auth-token exchange that follow it.
- Body-carrying POSTs to the PS cover content-digest and content-type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE
This was referenced Aug 12, 2026
Merged
Merged
Merged
Merge main (3af1cb4) into the -11 rewrite. Conflict resolution: - client/protocol.js, public/protocol.js: branch's -11 rewrite kept, then main's 97abdc8 re-applied on top — SIGNED_COMPONENTS / SIGNED_COMPONENTS_WITH_BODY constants with a signedComponents(hasBody) helper replace all twelve ad-hoc component arrays. Every signed request with a JSON body (bootstrap, refresh, notes authorize, plus the person-token and PS token calls that already had it) now covers content-type and content-digest; the seven bodyless sites use the four-component list. Also ports rebindPs/runRefresh(psOverride) from 97abdc8 so main's dev-mode PS picker (public/app.js) keeps working. - src/crypto.ts, src/index.ts, test/server.test.ts: branch side kept — it already signs with fully-specified Ed25519 and, per -11 §Signature Algorithms, deliberately refuses the polymorphic EdDSA that main's 3af1cb4 still accepts. public/protocol.js regenerated via npm run build:client. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQ2FCHHAnuJWF5TJB3838S
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.
The playground requires a person token at
/authorize, and the browser walkthrough gains a first hop and a second consent. This is the demo people watch to understand AAuth, so the narration changes matter as much as the server does.What changed — server
Person token at the authorization endpoint. The agent presents an
aa-person+jwtinSignature-Keyinstead of its agent token. A resource MUST have verified a person token before it issues a resource token, so the identity a resource token carries is PS-asserted rather than agent-asserted.typ,dwk: aauth-person.json, JWKS at{iss}/.well-known/{dwk},audequal to this resource,cnf.jwkequal to the request signing key.401 AAuth-Requirement: requirement=person-tokenwhen absent (noSignature-Key, or an agent token presented — the wrong credential is the "absent" case, not an error, because the agent needs telling which one this endpoint wants);400 invalid_person_tokenwhen present but bad.psbody parameter is gone. The person token'sissis the person server, so/authorizenow takes onlyscope.agentand gainsps,sub,person_token_jti,mission_s256andtenant, all copied from the verified person token.agent_jktstays.expis clamped to the person token's. The PS checksps,sub,mission_s256andtenantagainst the person token it issued and rejects on any mismatch or omission, so a droppedtenantmakes the token unredeemable for an org-affiliated person.Signature-Keynow states which AAuth token types it accepts.verifySigJwttakes a requiredacceptset with no default, enforced before anything else looks at the token./authorizeacceptsperson;/api/demoacceptsauth. A person token and a PS-issued auth token shareiss,dwk,aud,subandcnfand differ only intyp— a verifier that omits that check takes a credential carrying no authorization for one that carries it.algis the fully-specifiedEd25519, never the polymorphicEdDSA, on both emit and accept. §Signature Algorithms allows no transition, soJWT_ALG_PARAMSdropsEdDSArather than tolerating it. Thealgstrip beforecrypto.subtle.importKeyis unchanged and now carries a DO NOT REMOVE note — workerd rejects an OKP JWK whosealgis"Ed25519".token_endpoint→auth_token_endpoint.What changed — browser narration
The flow gains a first hop and a second consent, which is the part a viewer actually sees.
person_token_endpointbeforewhoamiand beforenotes/authorize, then presents it in place of the agent token.202whenever the person has not used the resource before — which is every first run of the demo. This was previously unhandled and treated as an error; the202 requirement=interactionpath is the common path in production, not an edge case. It is Wallet's recognition consent.startDeferredPollingtakestokenField(person_token|auth_token) andcopyPrefix, and onerunDeferredResponserenders the202for both legs — long-poll step, consent card, persisted pending record.stage, so a same-tab redirect to the PS resumes into the right leg. The person-token leg re-enters the ceremony atcontinueWhoami/continueNotesAuthorize, which run the resource call and the auth-token exchange that follow it.content-digestandcontent-type.This is one coordinated wave
Twelve PRs across twelve repositories implement AAuth -11 and R3 -02, built in parallel worktrees that could not see each other and reconciled in one integration pass.
Merging any one alone breaks the others. For this repo, concretely:
whoamiandnoteslive. Its walkthrough now fetches a person token before calling either. Merging this beforeaauth-dev/whoamiandaauth-dev/notesmeans the demo presents a person token to resources that still want an agent token; merging those first without this means they challenge withrequirement=person-tokenand the demo has no handler.HelloCoop/WalletshipsEd25519breaks/api/demooutright. Everyaa-auth+jwtandaa-person+jwtperson.hello.coopissues today carriesalg: EdDSA, andJWT_ALG_PARAMSno longer has an entry for it.mainauto-deploys. There is no staging, and this is the public demo.The twelve PRs
hellocoop/mockindickhardt/AAuthaauth-dev/packages-js@aauth/protocol1.0.0 (new),@aauth/agent3.0.0,@aauth/resource2.0.0, five more.aauth-dev/proxy@aauth/proxy1.0.0 — the agent-proxy core.hellocoop/aauth-proxyhellocoop/proxy-mcp@aauth/proxy^1.0.0; AP metadata cleanup.aauth-dev/notesaauth-dev/whoamiaauth-dev/web-agent-demo(playground)202deferred path.aauth-dev/playground-popupaauth-dev/registryaauth-dev/wwwRelated and already open:
aauth-dev/explorer#5 reworks the explorer for -11 / R3 -02.Background:
AAUTH-11-INTEGRATION.md(the integration ledger) andAAUTH-11-PACKAGE-CONTRACT.md(the pinned interface contract the parallel work packages were built against).Everyone who has used the playground re-consents. R3 documents drop
version, so the document bytes change, so every hash changes, so every grant keyed by hash is dead.The same cutover in
hellocoop/aauth-proxycosts users a second, larger thing: the connection store is rekeyed from a baresubtopersonId(ps, sub), so every stored upstream OAuth credential is orphaned — its key can no longer be computed. Users reconnect every service, and six times for GitHub, which splits into six resources in the same wave. Do not describe the cutover as "people re-consent."Ordering constraints
@aauth/protocol1.0.0 needs a manual first publish — npm will not register a trusted publisher for a name absent from the registry.@aauth/proxy1.0.0 must publish before the fleet servesaccess_mode: person-token. 0.4.0 hard-fails on an unrecognized value.HelloCoop/Walletsvr/issuer/sign.js:32must shipEd25519in the same window.const alg = useEdDSA ? 'EdDSA' : 'RS256',useEdDSA = isAAuthType(typ). RS256 stays for OIDC.Not covered by tests
expires_atclamp.mission_endpointis unimplemented by agreement. The reference PS accepts any value as amission_s256, so §Resource Token Verification step 7 (mission active, now beforeexpires_at) is never enforced and the clamp here is untested.environment: 'node', wherejoseaccepts the OKPalg: "Ed25519"that workerd'scrypto.subtle.importKeyrejects — breaking the strip passes CI and fails on deploy.src/crypto.tsis the canonical form of the strip for the whole fleet; the contract quotes it.wrangler dev. A[[routes]] custom_domainentry makes the dev server hand the Worker the production hostname, so@authoritynever matches what the client signed; httpsig reportsverified: falsewith noerrorstring, which reads like a bad key rather than a host mismatch.What a reviewer should check
tenantcopy-through. Step 6 has the PS verifyps,sub,mission_s256andtenantmatch exactly, rejecting on any mismatch or omission — so dropping it fails the exchange outright for a tenant-bearing person. (Checked:src/index.tscopies it, with the reasoning in a comment.)EdDSA. (Checked:EdDSAsurvives only in a test fixture asserting rejection.)'person'. (Checked:accept: ['person']appears only at/authorize;/api/demoisaccept: ['auth'].)alg-strip beforecrypto.subtle.importKeyis intact. It now carries a DO NOT REMOVE note — keep it. EmitEd25519, strip before import; opposite ends of the same pipe.One item still open
/.well-known/aauth-agent.jsonemits bothclient_nameandname.client_nameis an RFC 7591 borrowing that appears nowhere in the AAuth specs; -11 definesname. Dropping it is safe — Wallet'sentity-cache.jsreadsclient_name || name— but it was not dropped here, because967a5a1onmainis literally "Restore client_name in agent metadata" and reverting that blind seemed worse than flagging it. Someone who knows why that commit exists should decide. The same field is still emitted byaauth-dev/playground-popup;hellocoop/proxy-mcphas already made the switch toname.