Skip to content

AAuth -11: person token required, directed identity, fully-specified alg - #1

Merged
dickhardt merged 3 commits into
mainfrom
aauth-11/wp13-whoami
Aug 13, 2026
Merged

AAuth -11: person token required, directed identity, fully-specified alg#1
dickhardt merged 3 commits into
mainfrom
aauth-11/wp13-whoami

Conversation

@dickhardt

@dickhardt dickhardt commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

whoami answers two questions at one endpoint, and now demonstrates two of the five access modes rather than one. Adds the person-identity path for AAuth -11, enforces directed identity properly, and moves to the fully-specified Ed25519.

What changed

Agent identity is unchanged and stays. An agent token with no ?scope= is verified per §Agent Token Verification and returns the agent's own iss and sub. The agent token is the only token a resource reads that still carries an agent identifier — -11 removed agent from person, resource and auth tokens, not from here.

Person identity is the new work:

  • The aa-person+jwt branch: dwk, PS JWKS discovery, exp/iat, iss as a conforming server identifier, aud, and cnf.jwk against the request-signing key. A person token carrying scope or account is rejected. With no ?scope= the verified token is the answer; with ?scope= it becomes the basis of a resource token.
  • ?scope= on an agent token now gets 401 requirement=person-token. A resource MUST have verified a person token before it issues a resource token. Nothing is read or fetched from the agent token on that path — it would not change the answer, and following its iss would let an attacker drive outbound requests from the Worker.
  • Resource token: drops the agent claim; copies ps, sub and person_token_jti from the verified person token; keeps agent_jkt; copies mission_s256 and tenant when present; and never expires after the person token does — the most a resource can do about mission expiry, since it sees only the hash.
  • A person's identity is the pair (iss, sub). sub is opaque and unique only within its issuer, so identityRecordKey length-prefixes iss, and there is no path that produces a key from sub alone. iss is always released alongside sub. An agent identifier is a different shape — global and self-qualifying — and equally unparsed.
  • Emit and accept only Ed25519; the polymorphic EdDSA is rejected. Separately, alg is stripped before importSigningKeygenerate-key.mjs stamps alg: "Ed25519" on the SIGNING_KEY secret and workerd's importKey rejects that on an OKP key.
  • openapi.json rewritten for the full flow, declaring access_mode: agent-token — the minimum that gets an answer, so no agent skips this resource for a setup it does have. No x-aauth-access-mode annotation: R3 annotations are sparse and the one operation requires exactly the resource-wide mode.
  • New vitest coverage: signed requests end to end, both identity paths, every verification failure mode, resource token shape, mission pass-through, and the directed-identifier rule.

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:

  • Merging this before HelloCoop/Wallet ships Ed25519 makes whoami reject every live person token. Every aa-person+jwt and aa-auth+jwt person.hello.coop issues today carries alg: EdDSA; this branch rejects it outright. whoami would refuse every token the live PS issues.
  • aauth-dev/playground calls whoami first in its browser walkthrough, and now fetches a person token before doing so. Merging one without the other leaves the demo narrating a hop the resource does not require, or requiring a hop the demo does not perform.
  • This is a Cloudflare connected repo: a push to main auto-deploys. There is no staging.

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

whoami holds no grants of its own, so it costs its users nothing directly. The wave's flag day does, and this repo is part of the same cutover window:

  1. Re-consent, from the R3 hash rotation — R3 documents drop version, so the bytes change and every hash changes.
  2. Re-connect every upstream OAuth account. hellocoop/aauth-proxy rekeys its connection store from a bare sub to personId(ps, sub) — correct under -11, since a sub from one PS must never match a record established under another, and exactly the rule identityRecordKey implements here. Every existing tok: row 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.

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. Mission pass-through is tested; mission expiry is not.
  • 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. That risk is live here: generate-key.mjs stamps alg on the secret this worker imports.
  • 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

  • src/aauth.ts is a deliberate local copy, written to be deleted once @aauth/resource lands. It is still here: whoami and notes were written while @aauth/resource was still being built, and the interface contract pinned @aauth/protocol completely while saying nothing about @aauth/resource. Moving onto the package is a follow-up, not part of this PR — and it needs @aauth/resource to expose a createResourceToken that a caller holding only a record of the person token can satisfy (AAuth#90).
  • tenant copy-through. Step 6 has the PS verify ps, sub, mission_s256 and tenant match exactly, rejecting on any mismatch or omission. (Checked: whoami copies it.)
  • No verifier accepts EdDSA. (Checked: EdDSA appears in src/aauth.ts only inside the forbidden-alg set, with a test asserting isForbiddenAlg('EdDSA') is true.)
  • No API-serving path accepts 'person' where an auth token is required. A person token and a PS-issued auth token share iss, dwk, aud, sub and cnf and differ only in typ; without the check, a person token passes everything else and falls through to the scope gate as 403 insufficient_scope — a wrong-credential problem reported as a permissions problem.
  • The alg-strip before crypto.subtle.importKey is intact, on the signing key as well as verification keys.
  • identityRecordKey length-prefixes iss, and no code path derives a key from sub alone.

whoami answers two questions at one endpoint, and now shows two of the
five access modes rather than one.

Agent identity access is unchanged in -11 and stays: an agent token with
no ?scope= is verified per §Agent Token Verification and returns the
agent's own iss and sub. The agent token is the only token a resource
reads that still carries an agent identifier — -11 removed `agent` from
person, resource and auth tokens, not from here.

Person identity is the new work:

- Add the aa-person+jwt branch: dwk, PS JWKS discovery, exp/iat, iss as a
  conforming server identifier, aud, and cnf.jwk against the
  request-signing key. A person token carrying scope or account is
  rejected. With no ?scope= the verified token is the answer; with
  ?scope= it becomes the basis of a resource token.
- ?scope= on an agent token now gets 401 requirement=person-token: a
  resource MUST have verified a person token before it issues a resource
  token. Nothing is read or fetched from the agent token on that path —
  it would not change the answer, and following its iss would let an
  attacker drive outbound requests from the Worker.
- Resource token: drop the `agent` claim, which -11 removed. Copy ps,
  sub and person_token_jti from the verified person token, keep
  agent_jkt, copy mission_s256 and tenant when present, and never expire
  after the person token does — the most a resource can do about mission
  expiry, since it sees only the hash.
- A person's identity is the pair (iss, sub). sub is opaque and unique
  only within its issuer, so identityRecordKey length-prefixes iss and
  there is no path that produces a key from sub alone. iss is always
  released alongside sub. An agent identifier is a different shape —
  global and self-qualifying — and equally unparsed.
- Emit and accept only the RFC 9864 fully-specified Ed25519; the
  polymorphic EdDSA is rejected. This is a flag day: Wallet's
  svr/issuer/sign.js must ship Ed25519 for AAuth token types in the same
  window. Separately, strip alg before importSigningKey —
  generate-key.mjs stamps alg "Ed25519" on the SIGNING_KEY secret and
  workerd's importKey rejects that on an OKP key.
- openapi.json: rewrite for the full flow and declare access_mode
  agent-token, the minimum that gets an answer, so no agent skips this
  resource for a setup it does have. No x-aauth-access-mode annotation:
  R3 annotations are sparse and the one operation requires exactly the
  resource-wide mode.
- Add vitest coverage: signed requests end to end, both identity paths,
  every verification failure mode, resource token shape, mission
  pass-through, and the directed-identifier rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
whoami-aauth-dev c63265f Aug 13 2026, 03:07 PM

dickhardt and others added 2 commits August 13, 2026 15:36
Conflict in src/index.ts resource-token header: kept branch side
(SIGNING_ALG/TOKEN_TYP constants, exp clamped to person token).
Main's fully-specified Ed25519, workerd alg-strip, and httpsig 2.0.1
are all already present on this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQ2FCHHAnuJWF5TJB3838S
whoami is a resource with a single bodyless GET endpoint, so AAuth -11
§10.3's content-digest rule (body-carrying requests to PS or AS
endpoints) does not apply anywhere here: no requireContentDigest on
verification, and the four-component Accept-Signature challenge is
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQ2FCHHAnuJWF5TJB3838S
@dickhardt
dickhardt merged commit 9f023d2 into main Aug 13, 2026
1 check passed
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