From 04ba209adc463b86ff7f93a1dabf3d710bd6755a Mon Sep 17 00:00:00 2001 From: MarkXian Date: Fri, 17 Jul 2026 10:17:51 +0800 Subject: [PATCH] fix: honor per-request key override on jwtVerify and jwtSign A `key` passed in the per-request options of `request.jwtVerify` (and `reply.jwtSign`) was silently discarded: after the secret resolver ran, `mergeOptionsWithKey` overwrote the caller-provided `key` with the resolved registration secret, so the documented "key overrides secret" behaviour did not apply on these paths. Capture the caller-provided `key` before the options are merged and, when present, use it instead of the resolved secret. Registration-level secret resolution and global option precedence are unchanged. Closes #346 --- index.js | 20 ++++++++++++++++++-- test/jwt.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 3b7c973..32b8f1f 100644 --- a/index.js +++ b/index.js @@ -383,6 +383,10 @@ function fastifyJwt (fastify, options, next) { }) } + // Capture any per-request `key` before merging, so it can take precedence + // over the resolved secret (as documented for the `sign.key` option). + const requestSignKey = options.sign ? options.sign.key : options.key + if (options.sign) { const localSignOptions = convertTemporalProps(options.sign) // New supported contract, options supports sign and can expand @@ -409,7 +413,11 @@ function fastifyJwt (fastify, options, next) { }, function sign (secretOrPrivateKey, callback) { if (useLocalSigner) { - const signerOptions = mergeOptionsWithKey(options.sign || options, secretOrPrivateKey) + const localSignOptions = options.sign || options + // A per-request `key` overrides the resolved secret; otherwise the resolved secret is used. + const signerOptions = requestSignKey + ? localSignOptions + : mergeOptionsWithKey(localSignOptions, secretOrPrivateKey) const localSigner = createSigner(signerOptions) const token = localSigner(payload) callback(null, token) @@ -477,6 +485,10 @@ function fastifyJwt (fastify, options, next) { options = {} } + // Capture any per-request `key` before merging, so it can take precedence + // over the resolved secret (as documented for the `verify.key` option). + const requestVerifyKey = options.verify ? options.verify.key : options.key + if (options.decode || options.verify) { const localVerifyOptions = convertTemporalProps(options.verify, true) // New supported contract, options supports both decode and verify @@ -508,7 +520,11 @@ function fastifyJwt (fastify, options, next) { }, function verify (secretOrPublicKey, callback) { try { - const verifierOptions = mergeOptionsWithKey(options.verify || options, secretOrPublicKey) + const localVerifyOptions = options.verify || options + // A per-request `key` overrides the resolved secret; otherwise the resolved secret is used. + const verifierOptions = requestVerifyKey + ? localVerifyOptions + : mergeOptionsWithKey(localVerifyOptions, secretOrPublicKey) const localVerifier = getVerifier(verifierOptions, useGlobalOptions) const verifyResult = localVerifier(token) if (verifyResult && typeof verifyResult.then === 'function') { diff --git a/test/jwt.test.js b/test/jwt.test.js index c9a6823..1f6842b 100644 --- a/test/jwt.test.js +++ b/test/jwt.test.js @@ -3036,3 +3036,28 @@ test('local sign options should not overwrite global sign options', async functi t.assert.strictEqual(fastify.jwt.options.sign.expiresIn, '15m') }) + +test('request.jwtVerify should honor a per-request verify.key override', async function (t) { + t.plan(2) + + const fastify = Fastify() + fastify.register(jwt, { secret: 'hunter2' }) + + fastify.get('/verify', async function (request) { + return request.jwtVerify({ verify: { key: 'override' } }) + }) + + await fastify.ready() + + // Token signed with the override key, not the registration secret. + const token = createSigner({ key: 'override' })({ foo: 'bar' }) + + const response = await fastify.inject({ + method: 'get', + url: '/verify', + headers: { authorization: `Bearer ${token}` } + }) + + t.assert.strictEqual(response.statusCode, 200) + t.assert.strictEqual(JSON.parse(response.payload).foo, 'bar') +})