From 31459bc8f25055316bc6beb6866482ff3d1c242a Mon Sep 17 00:00:00 2001 From: dickhardt Date: Tue, 1 Sep 2026 20:58:53 +0100 Subject: [PATCH 1/2] Rename resource token claim person_token_jti to presented_jti (spec issue #95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec renamed the claim: the old name asserted the presented credential was a person token, which is false on step-up and per-call challenges. The value is unchanged — the jti of the person token whose verification established ps and sub. Transition strategy: createResourceToken dual-emits presented_jti (canonical) and person_token_jti (deprecated alias, same value) so PSes that still read the old name keep working; the alias goes away once the fleet's PSes read the new one. mockin 2.0.0 still resolves by the legacy name, and the e2e suite passing against it is the proof the dual-emit holds. No package in this repo reads the claim at runtime — the PS side lives elsewhere — so there is no accept-either read path to add here. Comments, docs and the e2e mint knob (forgePersonTokenJti → forgePresentedJti) now name presented_jti as canonical. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Hb3JdCGpyWkWVYsH51j1yU --- agent/src/token-exchange.ts | 2 +- e2e/aauth-protocol.test.ts | 7 +++++-- e2e/helpers.ts | 10 +++++----- resource/README.md | 8 ++++++-- resource/src/resource-token.test.ts | 15 ++++++++++++++- resource/src/resource-token.ts | 13 +++++++++---- 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/agent/src/token-exchange.ts b/agent/src/token-exchange.ts index 428ad59..6ff277a 100644 --- a/agent/src/token-exchange.ts +++ b/agent/src/token-exchange.ts @@ -254,7 +254,7 @@ export async function exchangeToken(options: TokenExchangeOptions): Promise { iss: RESOURCE, dwk: DWK.resource, aud: PS, - // ps, sub and person_token_jti are copied from the person token — this is + // ps, sub and presented_jti are copied from the person token — this is // what lets the PS resolve which person token this resource verified. + // person_token_jti is the pre-rename alias (spec issue #95), dual-emitted + // until every PS reads the new name. ps: person.iss, sub: person.sub, + presented_jti: person.jti, person_token_jti: person.jti, agent_jkt: agent.signingKey.thumbprint, scope: 'read', @@ -330,7 +333,7 @@ describe('the three-party flow, end to end', () => { // The jti store is what makes step 6 of §Resource Token Verification // possible at all. Clearing it is the same as a PS restart. const personToken = await getPersonToken() - resource.mint = { forgePersonTokenJti: '00000000-0000-0000-0000-000000000000' } + resource.mint = { forgePresentedJti: '00000000-0000-0000-0000-000000000000' } const resourceToken = await getResourceToken(personToken) const refused = await redeemExpectingRefusal(resourceToken) diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 69365ba..e223de3 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -500,8 +500,8 @@ export interface MintBehaviour { overrideTenant?: string /** Sign the resource token with the polymorphic `EdDSA`. */ alg?: string - /** Name a `person_token_jti` this PS never issued. */ - forgePersonTokenJti?: string + /** Name a `presented_jti` this PS never issued. */ + forgePresentedJti?: string scope?: string lifetimeSeconds?: number } @@ -592,7 +592,7 @@ export async function startResource(options: ResourceOptions): Promise { expect(typeof p.jti).toBe('string') expect(p.ps).toBe(PS) expect(p.sub).toBe('8f14e45fceea167a5a36dedd4bea2543') - expect(p.person_token_jti).toBe('pt-3ab910') + expect(p.presented_jti).toBe('pt-3ab910') expect(p.agent_jkt).toBe('NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs') expect(p.scope).toBe('notes.read notes.write') expect(p.iat).toBe(now) expect(p.exp).toBe(now + 300) }) + it('dual-emits presented_jti and its deprecated alias person_token_jti', async () => { + // Spec issue #95 renamed `person_token_jti` to `presented_jti`. Both are + // emitted with the same value until every PS reads the new name; the + // legacy claim goes away when the transition ends. + const { sign, captured } = capturingSign() + await createResourceToken(base(), sign) + const p = captured.payload! + + expect(p.presented_jti).toBe('pt-3ab910') + expect(p.person_token_jti).toBe('pt-3ab910') + expect(p.person_token_jti).toBe(p.presented_jti) + }) + it('carries none of the removed -10 claims', async () => { const { sign, captured } = capturingSign() await createResourceToken(base(), sign) diff --git a/resource/src/resource-token.ts b/resource/src/resource-token.ts index 2f83277..ed46bd5 100644 --- a/resource/src/resource-token.ts +++ b/resource/src/resource-token.ts @@ -22,7 +22,8 @@ export interface PersonTokenReference { iss: string /** `sub` of the person token — directed, opaque, meaningful only with `iss`. */ sub: string - /** `jti` of the person token — binds this resource token to that one. */ + /** `jti` of the person token — binds this resource token to that one. + * Emitted as `presented_jti` (and its pre-rename alias `person_token_jti`). */ jti: string /** Copied unchanged when present. A resource MUST NOT omit it. */ mission_s256?: string @@ -34,7 +35,7 @@ export interface ResourceTokenOptions { resource: string /** `aud` — the PS in three-party access, the AS in four-party. */ audience: string - /** The person token this resource verified. `ps`, `sub`, `person_token_jti`, + /** The person token this resource verified. `ps`, `sub`, `presented_jti`, * `mission_s256` and `tenant` are copied from it. */ personToken: VerifiedPersonToken | PersonTokenReference /** JWK thumbprint (RFC 7638) of the agent's current signing key. For a @@ -179,7 +180,11 @@ export async function createResourceToken( jti: randomId(), ps: person.iss, sub: person.sub, - person_token_jti: person.jti, + // `presented_jti` is the -11 name (spec issue #95); `person_token_jti` is + // its pre-rename alias, emitted alongside until every PS reads the new + // name. Same value: the jti of the person token this resource verified. + presented_jti: person.jti, + person_token_jti: person.jti, // deprecated alias of presented_jti agent_jkt: agentJkt, iat: now, exp, @@ -189,7 +194,7 @@ export async function createResourceToken( if (account !== undefined) payload.account = account // REQUIRED when the person token carried one, copied unchanged. A resource - // MUST NOT omit it: the PS resolves the person token by `person_token_jti` + // MUST NOT omit it: the PS resolves the person token by `presented_jti` // and compares, so dropping it is detected as mission stripping. if (person.mission_s256) payload.mission_s256 = person.mission_s256 From a0902b91206effb75274351d6aa7e2123050c990 Mon Sep 17 00:00:00 2001 From: dickhardt Date: Tue, 1 Sep 2026 21:45:44 +0100 Subject: [PATCH 2/2] resource 2.1.0: dual-emit presented_jti alongside person_token_jti Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Hb3JdCGpyWkWVYsH51j1yU --- package-lock.json | 2 +- resource/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f2a6a6..67b624c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4213,7 +4213,7 @@ }, "resource": { "name": "@aauth/resource", - "version": "2.0.1", + "version": "2.1.0", "license": "MIT", "dependencies": { "@aauth/interaction-code": "^0.1.0", diff --git a/resource/package.json b/resource/package.json index 0437d17..9bd4feb 100644 --- a/resource/package.json +++ b/resource/package.json @@ -1,6 +1,6 @@ { "name": "@aauth/resource", - "version": "2.0.1", + "version": "2.1.0", "description": "AAuth resource-side reference implementation: token verification, resource tokens, R3 documents and per-call proposals, challenge headers, interaction management", "type": "module", "exports": {