From 292507cddca84e05a5ba2d834a282d11c60e40f3 Mon Sep 17 00:00:00 2001 From: dickhardt Date: Tue, 1 Sep 2026 21:50:07 +0100 Subject: [PATCH] 2.0.1: accept presented_jti with person_token_jti fallback (spec issue #95) Resources dual-emit both names during the transition (@aauth/resource 2.1.0); the verifier prefers the canonical presented_jti and keeps accepting the pre-rename alias. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Hb3JdCGpyWkWVYsH51j1yU --- package-lock.json | 4 ++-- package.json | 2 +- src/aauth/verify-resource-token.js | 16 +++++++++++----- test/aauth/helpers.js | 9 +++++++++ test/aauth/token.errors.spec.js | 20 ++++++++++++++++++++ 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1daa5d6..aa8bf8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hellocoop/mockin", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hellocoop/mockin", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", "dependencies": { "@fastify/cors": "^10.0.0", diff --git a/package.json b/package.json index 52f25db..082cde2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hellocoop/mockin", "private": false, - "version": "2.0.0", + "version": "2.0.1", "description": "Hellō Mock Login OpenID Connect Server", "engines": { "node": "~22" diff --git a/src/aauth/verify-resource-token.js b/src/aauth/verify-resource-token.js index 56c9d5b..ae2e7cd 100644 --- a/src/aauth/verify-resource-token.js +++ b/src/aauth/verify-resource-token.js @@ -90,16 +90,22 @@ export async function verifyResourceToken( // ── Step 6 ───────────────────────────────────────────────────────── // The claims a resource copies out of the person token it verified. - if (!payload.person_token_jti) { - return { error: 'resource_token missing person_token_jti' } + // Spec issue #95 renamed `person_token_jti` → `presented_jti` (same + // value); resources dual-emit during the transition, so accept either, + // preferring the canonical name. + const presentedJti = payload.presented_jti ?? payload.person_token_jti + if (!presentedJti) { + return { + error: 'resource_token missing presented_jti (person_token_jti)', + } } if (!payload.ps) return { error: 'resource_token missing ps' } if (!payload.sub) return { error: 'resource_token missing sub' } - const issued = getPersonToken(payload.person_token_jti) + const issued = getPersonToken(presentedJti) if (!issued) { return { - error: `person_token_jti "${payload.person_token_jti}" names no person token this PS issued (or it has expired)`, + error: `presented_jti "${presentedJti}" names no person token this PS issued (or it has expired)`, } } if (payload.ps !== issued.ps) { @@ -143,7 +149,7 @@ export async function verifyResourceToken( scope: typeof payload.scope === 'string' ? payload.scope : '', ps: payload.ps, sub: payload.sub, - person_token_jti: payload.person_token_jti, + person_token_jti: presentedJti, mission_s256: payload.mission_s256 || null, tenant: payload.tenant || null, account: payload.account || null, diff --git a/test/aauth/helpers.js b/test/aauth/helpers.js index 8022d63..dbb7efb 100644 --- a/test/aauth/helpers.js +++ b/test/aauth/helpers.js @@ -137,6 +137,11 @@ export async function mintResourceToken({ r3_s256 = null, ttl = 300, personToken = null, + // Which name(s) carry the person-token jti (spec issue #95 rename): + // 'legacy' = person_token_jti only (pre-rename resources), + // 'presented' = presented_jti only (post-transition resources), + // 'both' = dual-emit (transition resources, @aauth/resource 2.1.0). + jti_claim = 'legacy', } = {}) { // Given a person token, copy from it — the normal case. Pass `false` // for any field to omit it deliberately (what a resource stripping a @@ -166,6 +171,10 @@ export async function mintResourceToken({ for (const k of ['ps', 'sub', 'person_token_jti']) { if (!payload[k]) delete payload[k] } + if (payload.person_token_jti && jti_claim !== 'legacy') { + payload.presented_jti = payload.person_token_jti + if (jti_claim === 'presented') delete payload.person_token_jti + } if (mission_s256) payload.mission_s256 = mission_s256 if (tenant) payload.tenant = tenant if (account) payload.account = account diff --git a/test/aauth/token.errors.spec.js b/test/aauth/token.errors.spec.js index 4ac4551..4e783e8 100644 --- a/test/aauth/token.errors.spec.js +++ b/test/aauth/token.errors.spec.js @@ -147,6 +147,26 @@ describe('AAuth auth_token_endpoint — errors', function () { expect(response.json().detail).to.match(/person_token_jti/) }) + it('accepts the renamed presented_jti claim alone (spec issue #95)', async function () { + const { person_token } = await getPersonToken(fastify) + const resourceToken = await mintResourceToken({ + personToken: person_token, + jti_claim: 'presented', + }) + const response = await postResourceToken(resourceToken) + expect(response.statusCode).to.equal(200) + }) + + it('accepts a dual-emit token carrying both jti claim names', async function () { + const { person_token } = await getPersonToken(fastify) + const resourceToken = await mintResourceToken({ + personToken: person_token, + jti_claim: 'both', + }) + const response = await postResourceToken(resourceToken) + expect(response.statusCode).to.equal(200) + }) + it('rejects a person_token_jti this PS never issued', async function () { const { person_token } = await getPersonToken(fastify) const resourceToken = await mintResourceToken({