Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agent/src/token-exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export async function exchangeToken(options: TokenExchangeOptions): Promise<Toke
}

// §Resource Token Verification rejections all land here — mission_s256 or
// tenant mismatch, an unknown person_token_jti, a prohibited alg. The server
// tenant mismatch, an unknown presented_jti, a prohibited alg. The server
// names which one; report it rather than the status alone.
throw new TokenExchangeError(response.status, await parseErrorBody(response))
}
Expand Down
7 changes: 5 additions & 2 deletions e2e/aauth-protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,13 @@ describe('the three-party flow, end to end', () => {
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',
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -592,7 +592,7 @@ export async function startResource(options: ResourceOptions): Promise<TestResou
/**
* The `jti` of the person token this resource most recently verified.
* AAuth issue #90: a per-call challenge fires on a request carrying an
* auth token, which has no `person_token_jti`, yet the resource token it
* auth token, which has no `presented_jti`, yet the resource token it
* must issue makes that claim REQUIRED — so a resource has to retain the
* person tokens it verified.
*/
Expand Down Expand Up @@ -857,7 +857,7 @@ export async function startResource(options: ResourceOptions): Promise<TestResou
personToken: {
iss: auth.ps,
sub: auth.sub,
// §Resource Token Structure makes `person_token_jti` REQUIRED,
// §Resource Token Structure makes `presented_jti` REQUIRED,
// but a per-call challenge fires on a request carrying an
// *auth* token, which has no such claim — AAuth issue #90. The
// resource retains the person token it verified and re-uses its
Expand Down Expand Up @@ -887,7 +887,7 @@ export async function startResource(options: ResourceOptions): Promise<TestResou
const ref: PersonTokenReference = {
iss: person.iss,
sub: person.sub,
jti: state.mint.forgePersonTokenJti ?? person.jti,
jti: state.mint.forgePresentedJti ?? person.jti,
}
// Honest path: copy `mission_s256` and `tenant` through unchanged.
// §Resource Token Structure — a resource MUST NOT omit either.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions resource/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const resourceToken = await createResourceToken(
{
resource: 'https://notes.example', // iss
audience: psUrl, // aud: the PS (three-party) or the AS (four-party)
personToken: verifiedPersonToken, // ps, sub, person_token_jti, mission_s256, tenant come from here
personToken: verifiedPersonToken, // ps, sub, presented_jti, mission_s256, tenant come from here
agentJkt: sig.thumbprint,
scope: 'notes.read notes.write',
kid: publicJwk.kid,
Expand All @@ -121,9 +121,13 @@ given — `alg` is the fully-specified RFC 9864 identifier, and the polymorphic
used.

`mission_s256` is copied from the person token unchanged and is REQUIRED when the person token
carried one; a resource MUST NOT omit it. The PS resolves the person token by `person_token_jti` and
carried one; a resource MUST NOT omit it. The PS resolves the person token by `presented_jti` and
compares, so dropping it is detected as mission stripping.

`presented_jti` is the claim's name since spec issue #95; the token also carries the deprecated
pre-rename alias `person_token_jti` with the same value, so a PS that has not picked up the rename
keeps working. The alias will be dropped once the transition ends.

`clampToMission(exp, missionExpiresAt)` is exported for anything else a resource derives from a
mission-scoped token: no token carrying `mission_s256` may outlive its mission.

Expand Down
2 changes: 1 addition & 1 deletion resource/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
15 changes: 14 additions & 1 deletion resource/src/resource-token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,26 @@ describe('createResourceToken', () => {
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)
Expand Down
13 changes: 9 additions & 4 deletions resource/src/resource-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand Down