AAuth -11: require a person token at /authorize - #2
Merged
Conversation
The agent now presents a PERSON token at the authorization endpoint, not
its agent token. A resource MUST have verified one before it issues a
resource token — the resource token names `ps`, `sub`, and
`person_token_jti`, and only the PS that issued that person token can
redeem it, so a resource token minted for an agent that cannot name a
person is one nobody can redeem.
- /authorize verifies typ, dwk, the issuer JWKS at {iss}/.well-known/{dwk},
aud, and cnf.jwk against the request-signing key. Missing or non-person
token → 401 AAuth-Requirement: requirement=person-token; malformed,
expired, or wrongly addressed → 400 invalid_person_token.
- The `ps` body parameter is gone. The person token's `iss` IS the person
server, and it is PS-asserted rather than agent-asserted.
- Resource token drops `agent`, gains `ps`, `sub`, `person_token_jti`,
keeps `agent_jkt`, and copies `mission_s256` and `tenant` when the
person token carried them. The PS checks all four against the person
token it issued and rejects on any mismatch OR omission, so for a person
with a tenant, dropping it fails the exchange outright. exp is clamped
to the person token's exp, which transitively keeps a mission-scoped
token inside its mission's window.
- psJwksVerifier takes a required `accept` set of token kinds, stated at
the call site: /api/demo accepts ['auth'] only. A person token and a
PS-issued auth token share iss, dwk, aud, sub and cnf and differ only in
typ, so without this a verifier that checks everything else takes a
credential carrying no authorization as though it carried some.
- alg: EdDSA → Ed25519 on the agent and resource token headers, and the
polymorphic identifier is no longer accepted on input either. The strip
before crypto.subtle.importKey stays — that is about the alg member, not
its value, and workerd rejects an OKP JWK carrying alg: "Ed25519".
- Browser narration gains the person-token hop: both flows POST to the
PS's person_token_endpoint before touching the resource, and handle the
202 deferred-consent response through the same long-poll machinery the
auth-token leg uses. PS metadata field token_endpoint →
auth_token_endpoint. Body-carrying PS requests now sign content-digest
and content-type.
The error table maps an expired person token to invalid_person_token
(400), but signature-key -08 bounds how long a Signature-Key token stays
usable and @hellocoop/httpsig rejects it first — a signature failure,
which -10 pins to 401. verifyPersonToken still checks exp itself, for the
case where httpsig's clock skew tolerance is wider than ours. A genuine
divergence between the spec's error table and what a conformant httpsig
implementation does first; the test pins which layer answers.
Flag day: Wallet's svr/issuer/sign.js:32 still heads every aa-auth+jwt and
aa-person+jwt with EdDSA, so /api/demo rejects live PS auth tokens until
that ships Ed25519 for AAuth token types.
Follow-up, not done here: -10 also makes it a verifier MUST that a JWKS
key's own `alg` be fully specified and agree with its kty/crv. Enforcing
that would reject keys published with `alg: EdDSA`, which un-migrated
issuers still serve, so it is a fleet-wide sweep with its own sequencing.
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 |
playground-popup-aauth-dev | 930db77 | Aug 13 2026, 02:40 PM |
This was referenced Aug 12, 2026
Merged
Merged
Merged
Beta wallet enforces AAuth -11 §10.3: a signed request carrying a body
to a PS or AS endpoint must cover content-digest (RFC 9530) and
content-type in addition to the four mandatory components, and rejects
anything else with "signature must cover content-digest on requests
with a body". Several body-carrying POSTs here (bootstrap, refresh,
notes authorize, the sig-fetch helpers, the notes API helper) signed
only ("@method" "@authority" "@path" "content-type" "signature-key")
or the bare four.
Replace the twelve ad-hoc component arrays with SIGNED_COMPONENTS /
SIGNED_COMPONENTS_WITH_BODY plus a signedComponents(hasBody) helper —
the same pattern web-agent-demo adopted in 97abdc8 — so the
body/digest pairing can't drift again. @hellocoop/httpsig derives and
sends the Content-Digest header itself whenever 'content-digest' is in
the component list, hashing the exact body string handed to it. Six
body-bearing sites now cover content-type + content-digest; six
bodyless sites keep the four-component list. Log-display mock headers
updated to match what actually goes on the wire.
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 popup playground requires a person token at
/authorize. Same protocol change asaauth-dev/playground, in the popup-flow variant, plus one genuine spec/implementation divergence found while doing it.What changed — server
The agent now presents a person token at the authorization endpoint, not its agent token. A resource MUST have verified one before it issues a resource token — the resource token names
ps,subandperson_token_jti, and only the PS that issued that person token can redeem it, so a resource token minted for an agent that cannot name a person is one nobody can redeem./authorizeverifiestyp,dwk, the issuer JWKS at{iss}/.well-known/{dwk},aud, andcnf.jwkagainst the request-signing key. Missing or non-person token →401 AAuth-Requirement: requirement=person-token; malformed, expired or wrongly addressed →400 invalid_person_token.psbody parameter is gone. The person token'sissis the person server, and it is PS-asserted rather than agent-asserted.agent, gainsps,sub,person_token_jti, keepsagent_jkt, and copiesmission_s256andtenantwhen the person token carried them. The PS checks all four against the person token it issued and rejects on any mismatch or omission, so for a person with a tenant, dropping it fails the exchange outright.expis clamped to the person token'sexp, which transitively keeps a mission-scoped token inside its mission's window.psJwksVerifiertakes a requiredacceptset of token kinds, stated at the call site:/api/demoaccepts['auth']only. A person token and a PS-issued auth token shareiss,dwk,aud,subandcnfand differ only intyp, so without this a verifier that checks everything else takes a credential carrying no authorization as though it carried some. Measured failure mode: it falls through to the scope gate as403 insufficient_scope— a wrong-credential problem reported as a permissions problem, which is the kind of thing nobody debugs correctly.alg: EdDSA→Ed25519on the agent and resource token headers, and the polymorphic identifier is no longer accepted on input either. The strip beforecrypto.subtle.importKeystays — that is about thealgmember, not its value, and workerd rejects an OKP JWK carryingalg: "Ed25519".What changed — browser narration
Both flows POST to the PS's
person_token_endpointbefore touching the resource, and handle the202deferred-consent response through the same long-poll machinery the auth-token leg already used. PS metadata fieldtoken_endpoint→auth_token_endpoint. Body-carrying PS requests now signcontent-digestandcontent-type.A genuine divergence, recorded
The error table maps an expired person token to
invalid_person_token(400), but signature-key -08 bounds how long aSignature-Keytoken stays usable and@hellocoop/httpsigrejects it first — a signature failure, which -10 pins to401.verifyPersonTokenstill checksexpitself, for the case where httpsig's clock-skew tolerance is wider than ours. This is a real divergence between the spec's error table and what a conformant httpsig implementation does first; the test pins which layer answers.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/api/demooutright. Everyaa-auth+jwtandaa-person+jwtperson.hello.coopissues today carriesalg: EdDSA, and this branch no longer accepts it. The demo would reject every live PS auth token.whoamiandnoteslive, and now presents a person token to both. Merging this withoutaauth-dev/whoamiandaauth-dev/notesmeans it presents a credential those resources do not yet expect.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 who has used the popup demo 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 and 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 theexpclamp here is untested.environment: 'node', wherejoseaccepts the OKPalg: "Ed25519"that workerd'scrypto.subtle.importKeyrejects — breaking the strip passes CI and fails on deploy.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.What a reviewer should check
tenantcopy-through. (Checked:src/index.tscopies it, with the "mismatch or omission" reasoning in a comment.)EdDSA. (Checked: it survives only in a test fixture asserting rejection.)'person'. (Checked:/api/demois['auth'].)alg-strip beforecrypto.subtle.importKeyis intact.Two items still open
/.well-known/aauth-resource.jsonemitsclient_nameand noname;/.well-known/aauth-agent.jsonemits both.client_nameis an RFC 7591 borrowing that appears nowhere in the AAuth specs — -11 definesnameas the common metadata field. Dropping it is safe (Wallet'sentity-cache.jsreadsclient_name || name), but the resource document currently publishes neither the field the spec defines. Worth fixing before this ships. The same field is still emitted byaauth-dev/playground, where a commit onmainexplicitly restored it and reverting blind seemed worse than flagging it.algbe fully specified and agree with itskty/crv. Enforcing that would reject keys published withalg: EdDSA, which un-migrated issuers still serve — so it is a fleet-wide sweep with its own sequencing, not a change to slip into this PR.