Skip to content

AAuth -11: require a person token at /authorize - #5

Merged
dickhardt merged 2 commits into
mainfrom
aauth-11/wp14-playground
Aug 13, 2026
Merged

AAuth -11: require a person token at /authorize#5
dickhardt merged 2 commits into
mainfrom
aauth-11/wp14-playground

Conversation

@dickhardt

@dickhardt dickhardt commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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+jwt in Signature-Key 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.

  • Verified per §Person Token Verification: typ, dwk: aauth-person.json, JWKS at {iss}/.well-known/{dwk}, aud equal to this resource, cnf.jwk equal to the request signing key. 401 AAuth-Requirement: requirement=person-token when absent (no Signature-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_token when present but bad.
  • The ps body parameter is gone. The person token's iss is the person server, so /authorize now takes only scope.
  • Resource token drops agent 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 reading a token from Signature-Key now states which AAuth token types it accepts. verifySigJwt takes a required accept set with no default, enforced 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 — 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 token_endpointauth_token_endpoint.

What changed — browser narration

The flow gains a first hop and a second consent, which is the part a viewer actually sees.

  • 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. This was previously unhandled and treated as an error; the 202 requirement=interaction path is the common path in production, not an edge case. It is Wallet's recognition consent.
  • 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.

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:

  • The playground drives whoami and notes live. Its walkthrough now fetches a person token before calling either. Merging this before aauth-dev/whoami and aauth-dev/notes means the demo presents a person token to resources that still want an agent token; merging those first without this means they challenge with requirement=person-token and the demo has no handler.
  • Merging this before HelloCoop/Wallet ships Ed25519 breaks /api/demo outright. Every aa-auth+jwt and aa-person+jwt person.hello.coop issues today carries alg: EdDSA, and JWT_ALG_PARAMS no longer has an entry for it.
  • This is a Cloudflare connected repo: a push to main auto-deploys. There is no staging, and this is the public demo.

The twelve PRs

Repo PR What it is
hellocoop/mockin #6 The reference PS. The only thing issuing -11 person tokens — the gate on verifying everything else.
dickhardt/AAuth #93 The specs: editorial corrections, plus six issues filed.
aauth-dev/packages-js #16 The npm surface. @aauth/protocol 1.0.0 (new), @aauth/agent 3.0.0, @aauth/resource 2.0.0, five more.
aauth-dev/proxy #2 @aauth/proxy 1.0.0 — the agent-proxy core.
hellocoop/aauth-proxy #33 The 38-proxy fleet. Carries the flag day. Merging deploys.
hellocoop/proxy-mcp #1 Tracks @aauth/proxy ^1.0.0; AP metadata cleanup.
aauth-dev/notes #2 Reference R3 resource; implements the #90 retention pattern.
aauth-dev/whoami #1 Agent identity + the new person-identity path.
aauth-dev/web-agent-demo (playground) #5 Browser walkthrough; handles the 202 deferred path.
aauth-dev/playground-popup #2 Popup-flow variant.
aauth-dev/registry #6 Five access modes; three-rung login.
aauth-dev/www #7 Six access-mode diagrams.

Related and already open: aauth-dev/explorer #5 reworks the explorer for -11 / R3 -02.

Background: AAUTH-11-INTEGRATION.md (the integration ledger) and AAUTH-11-PACKAGE-CONTRACT.md (the pinned interface contract the parallel work packages were built against).

⚠️ Flag day

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-proxy costs users a second, larger thing: the connection store is rekeyed from a bare sub to personId(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

  1. @aauth/protocol 1.0.0 needs a manual first publish — npm will not register a trusted publisher for a name absent from the registry.
  2. @aauth/proxy 1.0.0 must publish before the fleet serves access_mode: person-token. 0.4.0 hard-fails on an unrecognized value.
  3. HelloCoop/Wallet svr/issuer/sign.js:32 must ship Ed25519 in the same window. const alg = useEdDSA ? 'EdDSA' : 'RS256', useEdDSA = isAAuthType(typ). RS256 stays for OIDC.

Not covered by tests

  • Missions, and every expires_at clamp. mission_endpoint is unimplemented by agreement. The reference PS accepts any value as a mission_s256, so §Resource Token Verification step 7 (mission active, now before expires_at) is never enforced and the clamp here is untested.
  • Sub-agents, session tokens, revocation — no coverage in the wave.
  • Anything Workers-specific. Tests run under environment: 'node', where jose accepts the OKP alg: "Ed25519" that workerd's crypto.subtle.importKey rejects — breaking the strip passes CI and fails on deploy. src/crypto.ts is the canonical form of the strip for the whole fleet; the contract quotes it.
  • The signed happy path under wrangler dev. A [[routes]] custom_domain entry makes the dev server hand the Worker the production hostname, so @authority never matches what the client signed; httpsig reports verified: false with no error string, which reads like a bad key rather than a host mismatch.
  • Nothing has run against a live PS, because none issues -11 person tokens yet.

What a reviewer should check

  • tenant copy-through. Step 6 has the PS verify ps, sub, mission_s256 and tenant match exactly, rejecting on any mismatch or omission — so dropping it fails the exchange outright for a tenant-bearing person. (Checked: src/index.ts copies it, with the reasoning in a comment.)
  • No verifier accepts EdDSA. (Checked: EdDSA survives only in a test fixture asserting rejection.)
  • No API-serving path accepts 'person'. (Checked: accept: ['person'] appears only at /authorize; /api/demo is accept: ['auth'].)
  • The alg-strip before crypto.subtle.importKey is intact. It now carries a DO NOT REMOVE note — keep it. Emit Ed25519, strip before import; opposite ends of the same pipe.

One item still open

/.well-known/aauth-agent.json emits both client_name and name. client_name is an RFC 7591 borrowing that appears nowhere in the AAuth specs; -11 defines name. Dropping it is safe — Wallet's entity-cache.js reads client_name || name — but it was not dropped here, because 967a5a1 on main is 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 by aauth-dev/playground-popup; hellocoop/proxy-mcp has already made the switch to name.

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
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
@dickhardt
dickhardt merged commit c66f353 into main Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant