Skip to content
Open
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"dependencies": {
"@fastify/error": "^4.2.0",
"@lukeed/ms": "^2.0.2",
"fast-jwt": "^6.2.4",
"fast-jwt": "^6.3.3",
"fastify-plugin": "^6.0.0",
"steed": "^1.1.3"
},
Expand Down
83 changes: 83 additions & 0 deletions test/algorithm-confusion.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict'

const { test } = require('node:test')
const { createHmac, createPublicKey } = require('node:crypto')
const Fastify = require('fastify')
const jwt = require('..')

const helper = require('./helper')

const { publicKey } = helper.generateKeyPair()
const zeroWidthSpace = '\u200b'

function forgeHS256Token (secret, payload) {
const encode = part => Buffer.from(JSON.stringify(part)).toString('base64url')
const data = `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(payload)}`
const signature = createHmac('sha256', secret).update(data).digest('base64url')
return `${data}.${signature}`
}

async function verifyForgedToken (key) {
const fastify = Fastify()
fastify.register(jwt, { secret: async function () { return key } })

fastify.get('/protected', async function (request) {
return request.jwtVerify()
})

await fastify.ready()

const response = await fastify.inject({
method: 'GET',
url: '/protected',
headers: { authorization: `Bearer ${forgeHS256Token(key, { sub: 'attacker' })}` }
})

await fastify.close()
return response
}

test('public key material is not usable as an HMAC secret', async function (t) {
t.plan(3)

await t.test('serialized asymmetric JWK', async function (t) {
t.plan(2)

const jwk = JSON.stringify(createPublicKey(publicKey).export({ format: 'jwk' }))
const response = await verifyForgedToken(jwk)

t.assert.strictEqual(response.statusCode, 401)
t.assert.strictEqual(JSON.parse(response.payload).code, 'FST_JWT_AUTHORIZATION_TOKEN_INVALID')
})

await t.test('serialized asymmetric JWKS', async function (t) {
t.plan(2)

const jwks = JSON.stringify({ keys: [createPublicKey(publicKey).export({ format: 'jwk' })] })
const response = await verifyForgedToken(jwks)

t.assert.strictEqual(response.statusCode, 401)
t.assert.strictEqual(JSON.parse(response.payload).code, 'FST_JWT_AUTHORIZATION_TOKEN_INVALID')
})

await t.test('PEM hidden behind a zero width character', async function (t) {
t.plan(1)

const response = await verifyForgedToken(`${zeroWidthSpace}${publicKey}`)

t.assert.notStrictEqual(response.statusCode, 200, 'the forged HS256 token must not authenticate the request')
})
})

test('a symmetric JWK is still usable as an HMAC secret', async function (t) {
t.plan(1)

const fastify = Fastify()
fastify.register(jwt, { secret: JSON.stringify({ kty: 'oct', k: 'c3VwZXJzZWNyZXQ' }) })
await fastify.ready()

const token = fastify.jwt.sign({ foo: 'bar' })
t.assert.strictEqual(fastify.jwt.verify(token).foo, 'bar')

await fastify.close()
})
24 changes: 24 additions & 0 deletions test/jwt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3133,3 +3133,27 @@ test('local sign options should not overwrite global sign options', async functi

t.assert.strictEqual(fastify.jwt.options.sign.expiresIn, '15m')
})

test('"expiresIn" and "notBefore" take precedence over "exp" and "nbf" in the payload', async function (t) {
t.plan(6)

const fastify = Fastify()
fastify.register(jwt, { secret: 'test' })
await fastify.ready()

const oneHourInSeconds = 60 * 60
const nowInSeconds = Math.floor(Date.now() / 1000)
const payloadClaim = nowInSeconds + 24 * oneHourInSeconds

const withExpiresIn = fastify.jwt.decode(fastify.jwt.sign({ foo: 'bar', exp: payloadClaim }, { expiresIn: '1h' }))
t.assert.notStrictEqual(withExpiresIn.exp, payloadClaim)
t.assert.ok(Math.abs(withExpiresIn.exp - (nowInSeconds + oneHourInSeconds)) <= 5)

const withNotBefore = fastify.jwt.decode(fastify.jwt.sign({ foo: 'bar', nbf: payloadClaim }, { notBefore: '1h' }))
t.assert.notStrictEqual(withNotBefore.nbf, payloadClaim)
t.assert.ok(Math.abs(withNotBefore.nbf - (nowInSeconds + oneHourInSeconds)) <= 5)

const withoutSignOptions = fastify.jwt.decode(fastify.jwt.sign({ foo: 'bar', exp: payloadClaim, nbf: payloadClaim }))
t.assert.strictEqual(withoutSignOptions.exp, payloadClaim)
t.assert.strictEqual(withoutSignOptions.nbf, payloadClaim)
})
14 changes: 13 additions & 1 deletion test/options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const jwt = require('..')
const { AssertionError } = require('node:assert')

test('Options validation', async function (t) {
t.plan(3)
t.plan(4)

await t.test('Options are required', async function (t) {
t.plan(1)
Expand Down Expand Up @@ -144,4 +144,16 @@ test('Options validation', async function (t) {
})
})
})

await t.test('Verify options', async function (t) {
t.plan(3)

const fastify = Fastify()
await fastify.register(jwt, { secret: 'test' })
const token = fastify.jwt.sign({ foo: 'bar' })

t.assert.strictEqual(fastify.jwt.verify(token, { cacheTTL: Infinity }).foo, 'bar')
t.assert.throws(() => fastify.jwt.verify(token, { cacheTTL: NaN }), { code: 'FAST_JWT_INVALID_OPTION' })
t.assert.throws(() => fastify.jwt.verify(token, { clockTolerance: Infinity }), { code: 'FAST_JWT_INVALID_OPTION' })
})
})
Loading