From ad9317bd17e65af8bbfb3aa4ed2640db42a98b09 Mon Sep 17 00:00:00 2001 From: jmjf Date: Tue, 15 Oct 2024 12:06:54 -0400 Subject: [PATCH] fix: Async key provider and errors should be resolved internally -- dynamic JWTs in tests (#338) * test: Async key provider should be resolved internaly * test: Async key provider errors should be resolved internaly * test: Async key provider errors should be resolved internally * feat: Async key provider * test: generate JWTs dynamically --------- Co-authored-by: NikitaFedorov1 Co-authored-by: NikitaIT --- jwt.js | 50 ++++++++------- test/jwt-async.test.js | 135 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 test/jwt-async.test.js diff --git a/jwt.js b/jwt.js index 605c9ef..1b88173 100644 --- a/jwt.js +++ b/jwt.js @@ -486,34 +486,21 @@ function fastifyJwt (fastify, options, next) { }, function verify (secretOrPublicKey, callback) { try { + let verifyResult if (useLocalVerifier) { const verifierOptions = mergeOptionsWithKey(options.verify || options, secretOrPublicKey) const localVerifier = createVerifier(verifierOptions) - const verifyResult = localVerifier(token) - callback(null, verifyResult) + verifyResult = localVerifier(token) + } else { + verifyResult = verifier(token) + } + if (verifyResult && typeof verifyResult.then === 'function') { + verifyResult.then(result => callback(null, result), error => wrapError(error, callback)) } else { - const verifyResult = verifier(token) callback(null, verifyResult) } } catch (error) { - if (error.code === TokenError.codes.expired) { - return callback(new AuthorizationTokenExpiredError()) - } - - if (error.code === TokenError.codes.invalidKey || - error.code === TokenError.codes.invalidSignature || - error.code === TokenError.codes.invalidClaimValue - ) { - return callback(typeof messagesOptions.authorizationTokenInvalid === 'function' - ? new AuthorizationTokenInvalidError(error.message) - : new AuthorizationTokenInvalidError()) - } - - if (error.code === TokenError.codes.missingSignature) { - return callback(new AuthorizationTokenUnsignedError()) - } - - return callback(error) + return wrapError(error, callback) } }, function checkIfIsTrusted (result, callback) { @@ -542,6 +529,27 @@ function fastifyJwt (fastify, options, next) { } }) } + + function wrapError (error, callback) { + if (error.code === TokenError.codes.expired) { + return callback(new AuthorizationTokenExpiredError()) + } + + if (error.code === TokenError.codes.invalidKey || + error.code === TokenError.codes.invalidSignature || + error.code === TokenError.codes.invalidClaimValue + ) { + return callback(typeof messagesOptions.authorizationTokenInvalid === 'function' + ? new AuthorizationTokenInvalidError(error.message) + : new AuthorizationTokenInvalidError()) + } + + if (error.code === TokenError.codes.missingSignature) { + return callback(new AuthorizationTokenUnsignedError()) + } + + return callback(error) + } } module.exports = fp(fastifyJwt, { diff --git a/test/jwt-async.test.js b/test/jwt-async.test.js new file mode 100644 index 0000000..2c35bba --- /dev/null +++ b/test/jwt-async.test.js @@ -0,0 +1,135 @@ +'use strict' + +const test = require('tap').test +const Fastify = require('fastify') +const jwt = require('../jwt') +const { createSigner } = require('fast-jwt') + +test('Async key provider should be resolved internally', async function (t) { + const fastify = Fastify() + fastify.register(jwt, { + secret: { + private: 'supersecret', + public: async () => Promise.resolve('supersecret') + }, + verify: { + extractToken: (request) => request.headers.jwt, + key: () => Promise.resolve('supersecret') + } + }) + fastify.get('/', async function (request, reply) { + const token = await reply.jwtSign({ user: 'test' }) + request.headers.jwt = token + await request.jwtVerify() + return reply.send(request.user) + }) + const response = await fastify.inject({ + method: 'get', + url: '/', + headers: { + jwt: 'supersecret' + } + }) + t.ok(response) + t.comment("Should be 'undefined'") + t.match(response.json(), { user: 'test' }) +}) + +test('Async key provider errors should be resolved internally', async function (t) { + const fastify = Fastify() + fastify.register(jwt, { + secret: { + public: async () => Promise.resolve('key used per request, false not allowed') + }, + verify: { + extractToken: (request) => request.headers.jwt, + key: () => Promise.resolve('key not used') + } + }) + fastify.get('/', async function (request, reply) { + const signSync = createSigner({ key: 'invalid signature error' }) + request.headers.jwt = signSync({ sub: '1234567890', name: 'John Doe', iat: 1516239022 }) + // call to local verifier without cache + await request.jwtVerify() + return reply.send(typeof request.user.then) + }) + const response = await fastify.inject({ + method: 'get', + url: '/' + }) + + t.equal(response.statusCode, 401) +}) + +test('Async key provider should be resolved internally with cache', async function (t) { + const fastify = Fastify() + fastify.register(jwt, { + secret: { + private: 'this secret reused from cache', + public: async () => false + }, + verify: { + extractToken: (request) => request.headers.jwt, + key: () => Promise.resolve('this secret reused from cache') + } + }) + fastify.get('/', async function (request, reply) { + const signSync = createSigner({ key: 'this secret reused from cache' }) + request.headers.jwt = signSync({ sub: '1234567890', name: 'John Doe', iat: 1516239022 }) + await new Promise((resolve, reject) => request.jwtVerify((err, payload) => { + if (err) { + reject(err) + return + } + resolve(payload) + })) + await new Promise((resolve, reject) => request.jwtVerify((err, payload) => { + if (err) { + reject(err) + return + } + resolve(payload) + })) + return reply.send(request.user) + }) + const response = await fastify.inject({ + method: 'get', + url: '/' + }) + t.equal(response.statusCode, 200) + t.match(response.json(), { name: 'John Doe' }) +}) + +test('Async key provider errors should be resolved internally with cache', async function (t) { + const fastify = Fastify() + fastify.register(jwt, { + secret: { + public: async () => false + }, + verify: { + extractToken: (request) => request.headers.jwt, + key: () => Promise.resolve('this secret reused from cache') + } + }) + fastify.get('/', async function (request, reply) { + const signSync = createSigner({ key: 'invalid signature error' }) + request.headers.jwt = signSync({ sub: '1234567890', name: 'John Doe', iat: 1516239022 }) + // request.headers.jwt = + // 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + // call to plugin root level verifier + await new Promise((resolve, reject) => request.jwtVerify((err, payload) => { + if (err) { + reject(err) + return + } + resolve(payload) + })) + return reply.send(typeof request.user.then) + }) + const response = await fastify.inject({ + method: 'get', + url: '/' + }) + + t.equal(response.statusCode, 401) +})