Files
Filip Skokan 54c3627474 fix(types): correct key resolver and JWT header types
Match the protected header of each resolver callback to the value
supplied by its operation. Expose the members attached to local and
remote JWKS resolvers through named callable interfaces.

Allow `JWTHeaderParameters.b64` to be boolean. A false value is inert
unless "b64" is listed in "crit", so signing and verification accept it.

Signed-off-by: Filip Skokan <[email protected]>
2026-07-29 18:45:03 +02:00

121 lines
4.1 KiB
TypeScript

import test from 'ava'
import timekeeper from 'timekeeper'
import { setters } from './time_setters.js'
import { SignJWT, compactVerify, jwtVerify } from '../../src/index.js'
const now = 1604416038
test.before(async (t) => {
t.context.secret = new Uint8Array(32)
t.context.payload = { 'urn:example:claim': true }
timekeeper.freeze(now * 1000)
})
test.after(timekeeper.reset)
test('SignJWT', async (t) => {
const jwt = await new SignJWT(t.context.payload)
.setProtectedHeader({ alg: 'HS256' })
.sign(t.context.secret)
t.is(
jwt,
'eyJhbGciOiJIUzI1NiJ9.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6dHJ1ZX0.yPnOE--rxp3rJaYy0iZaW2Vswvus05G6_ZBdXqIdjGo',
)
})
test('SignJWT with default (empty) payload', async (t) => {
const jwt = await new SignJWT().setProtectedHeader({ alg: 'HS256' }).sign(t.context.secret)
t.is(jwt, 'eyJhbGciOiJIUzI1NiJ9.e30.4E_Bsx-pJi3kOW9wVXN8CgbATwP09D9V5gxh9-9zSZ0')
})
test('SignJWT w/crit', async (t) => {
const expected =
'eyJhbGciOiJIUzI1NiIsImNyaXQiOlsiaHR0cDovL29wZW5iYW5raW5nLm9yZy51ay9pYXQiXSwiaHR0cDovL29wZW5iYW5raW5nLm9yZy51ay9pYXQiOjB9.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6dHJ1ZX0.YzOrPZaNql7PpCo43HAJdj-LASP8lOmtb-Bzj9OrNAk'
await t.throwsAsync(
new SignJWT(t.context.payload)
.setProtectedHeader({
alg: 'HS256',
crit: ['http://openbanking.org.uk/iat'],
'http://openbanking.org.uk/iat': 0,
})
.sign(t.context.secret),
{
code: 'ERR_JOSE_NOT_SUPPORTED',
message: 'Extension Header Parameter "http://openbanking.org.uk/iat" is not recognized',
},
)
await t.notThrowsAsync(async () => {
const jwt = await new SignJWT(t.context.payload)
.setProtectedHeader({
alg: 'HS256',
crit: ['http://openbanking.org.uk/iat'],
'http://openbanking.org.uk/iat': 0,
})
.sign(t.context.secret, { crit: { 'http://openbanking.org.uk/iat': true } })
t.is(jwt, expected)
})
await t.throwsAsync(jwtVerify(expected, t.context.secret), {
code: 'ERR_JOSE_NOT_SUPPORTED',
message: 'Extension Header Parameter "http://openbanking.org.uk/iat" is not recognized',
})
await t.notThrowsAsync(
jwtVerify(expected, t.context.secret, { crit: { 'http://openbanking.org.uk/iat': true } }),
)
})
test('Signed JWTs cannot use unencoded payload', async (t) => {
await t.throwsAsync(
() =>
new SignJWT()
.setProtectedHeader({ alg: 'HS256', crit: ['b64'], b64: false })
.sign(t.context.secret),
{ code: 'ERR_JWT_INVALID', message: 'JWTs MUST NOT use unencoded payload' },
)
await t.throwsAsync(() => new SignJWT().sign(t.context.secret), {
code: 'ERR_JWS_INVALID',
message: 'either setProtectedHeader or setUnprotectedHeader must be called before #sign()',
})
})
test('"b64" is ignored when "crit" does not list it', async (t) => {
// Only a "b64" listed in "crit" is in effect per RFC 7797, so this one does not make the JWT an
// unencoded payload one and is signed like any other.
const jwt = await new SignJWT(t.context.payload)
.setProtectedHeader({ alg: 'HS256', b64: false })
.sign(t.context.secret)
const { protectedHeader } = await jwtVerify(jwt, t.context.secret)
t.deepEqual(protectedHeader, { alg: 'HS256', b64: false })
})
async function testJWTsetFunction(t, method, claim, value, expected = value) {
const jwt = await new SignJWT()
.setProtectedHeader({ alg: 'HS256' })
[method](value)
.sign(t.context.secret)
const { payload, key: resolvedKey } = await compactVerify(jwt, async (header, token) => {
t.true('alg' in header)
t.is(header.alg, 'HS256')
t.true('payload' in token)
t.true('protected' in token)
t.true('signature' in token)
return t.context.secret
})
t.is(resolvedKey, t.context.secret)
const claims = JSON.parse(new TextDecoder().decode(payload))
t.true(claim in claims)
t.is(claims[claim], expected)
}
testJWTsetFunction.title = (title, method, claim, value) =>
`SignJWT.prototype.${method} called with ${value?.constructor?.name || typeof value} (${value})`
for (const [method, claim, vectors] of setters(now)) {
for (const [input, output = input] of vectors) {
test(testJWTsetFunction, method, claim, input, output)
}
}