AAuth -11: five access modes, person token before resource token, Ed25519 - #6
Merged
Conversation
…5519
access_mode (types.ts, store.ts, validate.ts):
- `aauth-access-token` renamed `session-token` — the credential a resource
issues for its own consumption, returned in `AAuth-Access`.
- Added `person-token` and R3's `per-call`. The set is now the five values of
the AAuth Access Mode Value Registry, which -11 established with a
Specification Required policy.
- Extracted DEFAULT_ACCESS_MODE ('agent-token', the spec default) so the two
places that fall back to it cannot drift.
Validation scope: validate.ts keeps rejecting an access_mode outside the list,
deliberately. The field is an open registry, so this is the registry service
choosing what its directory will list — an editorial call, on the
registry-curation side. It does not reach agent behaviour: nothing on the
agent-provider (ap.ts), agent-token (agent-token.ts), or web-agent (client)
paths reads access_mode, and the UI renders an unknown value verbatim rather
than treating it as an error. Documented at each site.
Person token before resource token (login.ts) — the substantive protocol fix.
-11 §Resource Access: "A resource MUST have verified a person token before it
issues a resource token, and MUST challenge with `requirement=person-token`
when it has not." Only a PS can redeem a resource token, so one issued to an
agent that cannot name a person is unredeemable. The login flow was minting
resource tokens straight off an agent token, which is both wrong and the
reason it had nothing to put in `ps`/`sub`/`person_token_jti`.
The flow is now three rungs, at /auth/identity and POST /resources alike:
agent token → 401 `AAuth-Requirement: requirement=person-token`
person token → verified, then 401 `requirement=auth-token` + resource token
auth token → verified; identity read, session set
verifyPersonToken() implements §Person Token Verification: `typ`
`aa-person+jwt`, `dwk` `aauth-person.json`, JWKS at `{iss}/.well-known/{dwk}`
with the byte-equal issuer check, `aud` = our ORIGIN, `cnf.jwk` = the key that
signed the request, plus exp/iat, sub, jti, and the MUST NOT on scope/account.
Failures are `400 invalid_person_token`. Payload checks run before the
signature step so a structurally invalid token never causes an outbound fetch
to a host it names. The resource token now carries `ps`, `sub` and
`person_token_jti` copied from that token, and still no `agent` claim.
A person token alone does not attribute a submission: it is a directed
`(iss, sub)` with no scope, so it cannot release the verified email and name
an entry is credited with. It earns the resource token; the auth token
carries the claims.
This does not change the registry's own access_mode — listing still requires
only an agent token.
Signature algorithms (crypto.ts and its three callers): -11 requires a
fully-specified `alg` everywhere and forbids the polymorphic `EdDSA`.
- New SIGNING_ALG = 'Ed25519', used by the resource token (login.ts), the
agent token (ap.ts), and the session cookie (session.ts). The last is this
service's own format, not an AAuth token type, but it shares the key and
the verifier, so it moves too.
- verifyJWT no longer accepts `EdDSA`: the entry is gone from JWT_ALG_PARAMS,
so such a JWT fails with `unsupported alg: EdDSA`. A JWKS key whose own
algorithm contradicts the header is no longer a candidate.
- The two `alg` strips before crypto.subtle.importKey stay exactly as they
were. workerd rejects an OKP JWK whose alg is "Ed25519", and the algorithm
is passed explicitly via the import parameters. Emit Ed25519, strip before
import — opposite ends of the same pipe.
- Flag-day blocker recorded in the code: HelloCoop/Wallet/svr/issuer/sign.js:32
still signs every AAuth token with `EdDSA`, and must ship `Ed25519` in the
same window or auth tokens stop verifying here.
Client (client/registry.js, rebuilt public/registry.js): the browser web-agent
climbs the same ladder — person token from the PS's `person_token_endpoint`,
then the resource token, then the auth token from `auth_token_endpoint`.
Either PS step can defer with a 202, so the saved pending state records which
rung deferred and resumePending() picks up from there.
Display (discoverability.ts, client, README): all five values described, with
the note that an agent meeting an unrecognized value proceeds as with no
declaration, and that a resource may state a mode per operation via an R3
operation access annotation in its own vocabulary.
scripts/test.sh: assert the published JWK carries alg "Ed25519".
CLAUDE.md: the login ladder, and why signed requests fail under plain
`wrangler dev` (the custom_domain route rewrites the authority the Worker
sees, and httpsig reports the mismatch with no error string).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
registry-aauth-dev | f33585c | Aug 13 2026, 03:07 PM |
This was referenced Aug 12, 2026
Merged
Merged
Main's hotfix 6ffbc6b (fully-specified alg Ed25519 for signing and verification) is subsumed by this branch: SIGNING_ALG = 'Ed25519' in src/crypto.ts stamps every minted JWT header, JWT_ALG_PARAMS accepts Ed25519 (and deliberately drops EdDSA per AAuth -11 §Signature Algorithms), and alg/key_ops/ext are stripped before every WebCrypto importKey. httpsig 2.0.1 (workerd importKey fix, af364ff) was already an ancestor. All four conflicts resolved to the branch side; the merge result is tree-identical to the branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQ2FCHHAnuJWF5TJB3838S
Bump @hellocoop/httpsig to ^2.2.0 and apply AAuth -11 §10.3: body-carrying requests to token-issuing endpoints MUST cover content-digest and content-type; resources are exempt. - client/registry.js: drop the hand-rolled component lists in signedFetch and bootstrap — httpsig 2.2's defaults are the same lists, and its contentDigest: 'auto' default appends content-digest for string bodies. That makes the browser agent's POSTs to PS token endpoints (person_token, auth_token) §10.3-compliant. public/registry.js rebuilt. - src/ap.ts: /bootstrap mints agent tokens, so verifySigHwk now passes requireContentDigest: true. - src/agent-token.ts: the /resources API stays exempt (resource role) — verification unchanged — but the with-body Accept-Signature challenge now advertises content-digest + content-type, matching what a 2.2 client signs by default. - src/login.ts: unchanged — GET /auth/identity is bodyless and POST /resources is the resource role. 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 registry learns the five -11 access modes, and its login flow gets the protocol fix it was missing: a person token before a resource token.
What changed
access_mode— five values, not fouraauth-access-tokenrenamedsession-token— the credential a resource issues for its own consumption, returned inAAuth-Access.person-tokenand R3'sper-call. The set is now the five values of the AAuth Access Mode Value Registry, which -11 established with a Specification Required policy.DEFAULT_ACCESS_MODEextracted (agent-token, the spec default) so the two places that fall back to it cannot drift.Validation scope, deliberately.
validate.tskeeps rejecting anaccess_modeoutside the list. The field is an open registry, so this is the registry service choosing what its directory will list — an editorial call on the registry-curation side. It does not reach agent behaviour: nothing on the agent-provider (ap.ts), agent-token (agent-token.ts) or web-agent (client) paths readsaccess_mode, and the UI renders an unknown value verbatim rather than treating it as an error. Documented at each site.Person token before resource token — the substantive protocol fix
-11 §Resource Access: "A resource MUST have verified a person token before it issues a resource token, and MUST challenge with
requirement=person-tokenwhen it has not." Only a PS can redeem a resource token, so one issued to an agent that cannot name a person is unredeemable. The login flow was minting resource tokens straight off an agent token — which is both wrong and the reason it had nothing to put inps/sub/person_token_jti.The flow is now three rungs, at
/auth/identityandPOST /resourcesalike:verifyPersonToken()implements §Person Token Verification:typ: aa-person+jwt,dwk: aauth-person.json, JWKS at{iss}/.well-known/{dwk}with the byte-equal issuer check,aud= ourORIGIN,cnf.jwk= the key that signed the request, plusexp/iat,sub,jti, and the MUST NOT onscope/account. Failures are400 invalid_person_token. Payload checks run before the signature step, so a structurally invalid token never causes an outbound fetch to a host it names. The resource token now carriesps,subandperson_token_jticopied from that token, and still noagentclaim.A person token alone does not attribute a submission: it is a directed
(iss, sub)with no scope, so it cannot release the verified email and name an entry is credited with. It earns the resource token; the auth token carries the claims.This does not change the registry's own
access_mode— listing still requires only an agent token.Signature algorithms
-11 requires a fully-specified
algeverywhere and forbids the polymorphicEdDSA.SIGNING_ALG = 'Ed25519', used by the resource token (login.ts), the agent token (ap.ts) and the session cookie (session.ts). The last is this service's own format, not an AAuth token type, but it shares the key and the verifier, so it moves too.verifyJWTno longer acceptsEdDSA: the entry is gone fromJWT_ALG_PARAMS, so such a JWT fails withunsupported alg: EdDSA. A JWKS key whose own algorithm contradicts the header is no longer a candidate.algstrips beforecrypto.subtle.importKeystay exactly as they were. workerd rejects an OKP JWK whosealgis"Ed25519", and the algorithm is passed explicitly via the import parameters. EmitEd25519, strip before import — opposite ends of the same pipe.Client and display
client/registry.js(and the rebuiltpublic/registry.js): the browser web-agent climbs the same ladder — person token from the PS'sperson_token_endpoint, then the resource token, then the auth token fromauth_token_endpoint. Either PS step can defer with a202, so the saved pending state records which rung deferred andresumePending()picks up from there.discoverability.ts, client and README describe all five values, with the note that an agent meeting an unrecognized value proceeds as with no declaration, and that a resource may state a mode per operation via an R3 operation access annotation in its own vocabulary.scripts/test.shasserts the published JWK carriesalg: "Ed25519".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:
HelloCoop/WalletshipsEd25519breaks login entirely. Everyaa-auth+jwtandaa-person+jwtperson.hello.coopissues today carriesalg: EdDSA, andJWT_ALG_PARAMSno longer has an entry for it. Nobody could sign in.person-tokenandper-call— the modeshellocoop/aauth-proxystarts publishing in this same wave. Merging the fleet first means 38 resources publish a mode this service rejects at validation.mainauto-deploys. There is no staging.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 signed in to the registry signs in again, through a login flow that now has an extra rung and can defer twice.
The wave's larger cost lands in
hellocoop/aauth-proxy: R3 documents dropversion, so every hash changes and every grant keyed by hash is dead (re-consent); and 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. The blocker is recorded in the code.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.environment: 'node', wherejoseaccepts the OKPalg: "Ed25519"that workerd'scrypto.subtle.importKeyrejects — breaking either strip passes CI and fails on deploy.wrangler dev. This is the repo that found it: a[[routes]] custom_domainentry makes the dev server hand the Worker a URL whose host is the production hostname, so@authoritynever matches what the client signed. httpsig then reportsverified: falsewith noerrorstring, which reads like a bad key rather than a host mismatch. It cost several wasted rounds and it is written up inCLAUDE.md. It affectsnotes,whoami,playground,playground-popup,shipper,wwwand 20 of the aauth-proxy fleet'swrangler.tomlfiles — effectively everything.What a reviewer should check
EdDSA. (Checked: gone fromJWT_ALG_PARAMS.)'person'. The three-rung ladder means/auth/identityandPOST /resourcesaccept a person token only to issue a resource token, never to serve the request. Confirm the rungs cannot be short-circuited.alg-strips beforecrypto.subtle.importKeyare intact —importSigningKeyandverifyJWT.fullySpecifiedAlginsrc/crypto.tsopens withif (jwk.alg && jwk.alg !== 'EdDSA') return jwk.alg. That is fine as an emit-side normalizer, which is all it is used for, but note it does not enforce -10's verifier MUST that a key'skty/crvagree with itsalg— a JWK claimingalg: 'ES256'on anOKP/Ed25519key would pass through. The verify path handles this separately by dropping candidates whose algorithm contradicts the header; worth confirming that is the only path that matters here.issnever drives an outbound fetch.