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