From 2e734cbe483afd674567da992c881ea4c98d2447 Mon Sep 17 00:00:00 2001 From: Thomas Heymann <190132+thomheymann@users.noreply.github.com> Date: Tue, 19 May 2020 10:43:14 +0100 Subject: [PATCH] Add ability to customise verification token (#92) Co-authored-by: Thomas Heymann --- README.md | 1 + index.d.ts | 8 +++---- jwt.js | 12 +++++++--- test.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ type.test.ts | 19 +++++++--------- 5 files changed, 84 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 51b5957..0135d4b 100644 --- a/README.md +++ b/README.md @@ -417,6 +417,7 @@ fastify.listen(3000, err => { * `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers * `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons. +* `extractToken(request): token`: Callback function allowing to use custom logic to extract the JWT token from the request. #### messages options diff --git a/index.d.ts b/index.d.ts index 4c662fc..b1ba009 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,7 +8,7 @@ declare module 'fastify' { type VerifyPayloadType = object | string; type DecodePayloadType = object | string; - interface SignCallback extends jwt.SignCallback {} + interface SignCallback extends jwt.SignCallback { } interface VerifyCallback extends jwt.VerifyCallback { (err: jwt.VerifyErrors, decoded: Decoded): void; @@ -63,8 +63,8 @@ declare interface FastifyJWTOptions { secret: jwt.Secret | { public: jwt.Secret; private: jwt.Secret }; decode?: jwt.DecodeOptions; sign?: jwt.SignOptions; - verify?: jwt.VerifyOptions; - cookie?: { + verify?: jwt.VerifyOptions & { extractToken?: (request: fastify.FastifyRequest) => string | void; }; + cookie?: { cookieName: string; }; messages?: { @@ -74,7 +74,7 @@ declare interface FastifyJWTOptions { authorizationTokenInvalid?: ((err: Error) => string) | string; authorizationTokenUntrusted?: string; } - trusted?: (request: fastify.FastifyRequest, decodedToken: {[k: string]: any}) => boolean | Promise + trusted?: (request: fastify.FastifyRequest, decodedToken: { [k: string]: any }) => boolean | Promise } declare const fastifyJWT: fastify.Plugin; diff --git a/jwt.js b/jwt.js index 4f83016..04633e1 100644 --- a/jwt.js +++ b/jwt.js @@ -66,7 +66,7 @@ function fastifyJwt (fastify, options, next) { signOptions.algorithm && signOptions.algorithm.includes('RS') && (typeof secret === 'string' || - secret instanceof Buffer) + secret instanceof Buffer) ) { return next(new Error('RSA Signatures set as Algorithm in the options require a private and public key to be set as the secret')) } @@ -75,7 +75,7 @@ function fastifyJwt (fastify, options, next) { signOptions.algorithm && signOptions.algorithm.includes('ES') && (typeof secret === 'string' || - secret instanceof Buffer) + secret instanceof Buffer) ) { return next(new Error('ECDSA Signatures set as Algorithm in the options require a private and public key to be set as the secret')) } @@ -202,7 +202,13 @@ function fastifyJwt (fastify, options, next) { } let token - if (request.headers && request.headers.authorization) { + const extractToken = options.extractToken + if (extractToken) { + token = extractToken(request) + if (!token) { + return next(new BadRequest(messagesOptions.badRequestErrorMessage)) + } + } else if (request.headers && request.headers.authorization) { const parts = request.headers.authorization.split(' ') if (parts.length === 2) { const scheme = parts[0] diff --git a/test.js b/test.js index 80447df..18769a5 100644 --- a/test.js +++ b/test.js @@ -1919,3 +1919,65 @@ test('custom response messages', function (t) { }) }) }) + +test('extract custom token', function (t) { + t.plan(2) + + const fastify = Fastify() + fastify.register(jwt, { secret: 'test', verify: { extractToken: (request) => request.headers.customauthheader } }) + + fastify.post('/sign', function (request, reply) { + return reply.jwtSign(request.body) + .then(function (token) { + return { token } + }) + }) + + fastify.get('/verify', function (request, reply) { + return request.jwtVerify() + .then(function (decodedToken) { + return reply.send(decodedToken) + }) + }) + + t.test('token can be extracted correctly', function (t) { + t.plan(2) + fastify.inject({ + method: 'post', + url: '/sign', + payload: { foo: 'bar' } + }).then(function (signResponse) { + const token = JSON.parse(signResponse.payload).token + t.ok(token) + + return fastify.inject({ + method: 'get', + url: '/verify', + headers: { + customauthheader: token + } + }).then(function (verifyResponse) { + t.is(verifyResponse.statusCode, 200) + }) + }) + }) + + t.test('token can not be extracted', function (t) { + t.plan(2) + fastify.inject({ + method: 'post', + url: '/sign', + payload: { foo: 'bar' } + }).then(function (signResponse) { + const token = JSON.parse(signResponse.payload).token + t.ok(token) + + return fastify.inject({ + method: 'get', + url: '/verify' + }).then(function (verifyResponse) { + t.is(verifyResponse.statusCode, 400) + }) + }) + }) +}) diff --git a/type.test.ts b/type.test.ts index ce3e0f8..70f6a5d 100644 --- a/type.test.ts +++ b/type.test.ts @@ -12,7 +12,8 @@ app.register(fastifyJwt, { cookieName: 'jwt' }, verify: { - maxAge: '1h' + maxAge: '1h', + extractToken: (request) => 'token' }, decode: { complete: true @@ -23,24 +24,20 @@ app.register(fastifyJwt, { authorizationTokenExpiredMessage: 'Token Expired', authorizationTokenInvalid: (err) => `${err.message}`, authorizationTokenUntrusted: 'Token untrusted' - }, - trusted: () => true, + }, + trusted: () => true, }); -app.addHook("preHandler", async (request, reply) => -{ - try - { +app.addHook("preHandler", async (request, reply) => { + try { await request.jwtVerify(); } - catch (err) - { + catch (err) { reply.send(err); } }); -app.post('/signup', async (req, reply) => -{ +app.post('/signup', async (req, reply) => { const token = app.jwt.sign({ user: "userName" }); let data = await app.jwt.verify(token); const user = req.user;