You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					503 lines
				
				15 KiB
			
		
		
			
		
	
	
					503 lines
				
				15 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | const fp = require('fastify-plugin') | ||
|  | const { createSigner, createDecoder, createVerifier, TokenError } = require('fast-jwt') | ||
|  | const assert = require('assert') | ||
|  | const steed = require('steed') | ||
|  | const { parse } = require('@lukeed/ms') | ||
|  | const { | ||
|  |   BadRequest, | ||
|  |   Unauthorized | ||
|  | } = require('http-errors') | ||
|  | 
 | ||
|  | const messages = { | ||
|  |   badRequestErrorMessage: 'Format is Authorization: Bearer [token]', | ||
|  |   badCookieRequestErrorMessage: 'Cookie could not be parsed in request', | ||
|  |   noAuthorizationInHeaderMessage: 'No Authorization was found in request.headers', | ||
|  |   noAuthorizationInCookieMessage: 'No Authorization was found in request.cookies', | ||
|  |   authorizationTokenExpiredMessage: 'Authorization token expired', | ||
|  |   authorizationTokenInvalid: (err) => `Authorization token is invalid: ${err.message}`, | ||
|  |   authorizationTokenUntrusted: 'Untrusted authorization token' | ||
|  | } | ||
|  | 
 | ||
|  | function wrapStaticSecretInCallback (secret) { | ||
|  |   return function (request, payload, cb) { | ||
|  |     return cb(null, secret) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function convertToMs (time) { | ||
|  |   // by default if time is number we assume that they are seconds - see README.md
 | ||
|  |   if (typeof time === 'number') { | ||
|  |     return time * 1000 | ||
|  |   } | ||
|  |   return parse(time) | ||
|  | } | ||
|  | 
 | ||
|  | function convertTemporalProps (options, isVerifyOptions) { | ||
|  |   if (options && typeof options !== 'function') { | ||
|  |     if (isVerifyOptions && options.maxAge) { | ||
|  |       options.maxAge = convertToMs(options.maxAge) | ||
|  |     } else if (options.expiresIn || options.notBefore) { | ||
|  |       if (options.expiresIn) { | ||
|  |         options.expiresIn = convertToMs(options.expiresIn) | ||
|  |       } | ||
|  | 
 | ||
|  |       if (options.notBefore) { | ||
|  |         options.notBefore = convertToMs(options.notBefore) | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return options | ||
|  | } | ||
|  | 
 | ||
|  | function fastifyJwt (fastify, options, next) { | ||
|  |   if (!options.secret) { | ||
|  |     return next(new Error('missing secret')) | ||
|  |   } | ||
|  | 
 | ||
|  |   if (options.options) { | ||
|  |     return next(new Error('options prefix is deprecated')) | ||
|  |   } | ||
|  | 
 | ||
|  |   const secret = options.secret | ||
|  |   const trusted = options.trusted | ||
|  |   let secretOrPrivateKey | ||
|  |   let secretOrPublicKey | ||
|  | 
 | ||
|  |   if (typeof secret === 'object' && !Buffer.isBuffer(secret)) { | ||
|  |     if (!secret.private || !secret.public) { | ||
|  |       return next(new Error('missing private key and/or public key')) | ||
|  |     } | ||
|  |     secretOrPrivateKey = secret.private | ||
|  |     secretOrPublicKey = secret.public | ||
|  |   } else { | ||
|  |     secretOrPrivateKey = secretOrPublicKey = secret | ||
|  |   } | ||
|  | 
 | ||
|  |   let secretCallbackSign = secretOrPrivateKey | ||
|  |   let secretCallbackVerify = secretOrPublicKey | ||
|  |   if (typeof secretCallbackSign !== 'function') { | ||
|  |     secretCallbackSign = wrapStaticSecretInCallback(secretCallbackSign) | ||
|  |   } | ||
|  |   if (typeof secretCallbackVerify !== 'function') { | ||
|  |     secretCallbackVerify = wrapStaticSecretInCallback(secretCallbackVerify) | ||
|  |   } | ||
|  | 
 | ||
|  |   const cookie = options.cookie | ||
|  |   const formatUser = options.formatUser | ||
|  | 
 | ||
|  |   const decodeOptions = options.decode || {} | ||
|  |   const signOptions = convertTemporalProps(options.sign) || {} | ||
|  |   const verifyOptions = convertTemporalProps(options.verify, true) || {} | ||
|  |   const messagesOptions = Object.assign({}, messages, options.messages) | ||
|  |   const namespace = typeof options.namespace === 'string' ? options.namespace : undefined | ||
|  | 
 | ||
|  |   if ( | ||
|  |     signOptions && | ||
|  |     signOptions.algorithm && | ||
|  |     signOptions.algorithm.includes('RS') && | ||
|  |     (typeof secret === 'string' || | ||
|  |       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')) | ||
|  |   } | ||
|  |   if ( | ||
|  |     signOptions && | ||
|  |     signOptions.algorithm && | ||
|  |     signOptions.algorithm.includes('ES') && | ||
|  |     (typeof secret === 'string' || | ||
|  |       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')) | ||
|  |   } | ||
|  | 
 | ||
|  |   const jwtConfig = { | ||
|  |     decode: decode, | ||
|  |     options: { | ||
|  |       decode: decodeOptions, | ||
|  |       sign: signOptions, | ||
|  |       verify: verifyOptions, | ||
|  |       messages: messagesOptions | ||
|  |     }, | ||
|  |     cookie: cookie, | ||
|  |     sign: sign, | ||
|  |     verify: verify, | ||
|  |     lookupToken: lookupToken | ||
|  |   } | ||
|  | 
 | ||
|  |   let jwtDecodeName = 'jwtDecode' | ||
|  |   let jwtVerifyName = 'jwtVerify' | ||
|  |   let jwtSignName = 'jwtSign' | ||
|  |   if (namespace) { | ||
|  |     if (!fastify.jwt) { | ||
|  |       fastify.decorateRequest('user', null) | ||
|  |       fastify.decorate('jwt', Object.create(null)) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (fastify.jwt[namespace]) { | ||
|  |       return next(new Error(`JWT namespace already used "${namespace}"`)) | ||
|  |     } | ||
|  |     fastify.jwt[namespace] = jwtConfig | ||
|  | 
 | ||
|  |     jwtDecodeName = options.jwtDecode ? (typeof options.jwtDecode === 'string' ? options.jwtDecode : 'jwtDecode') : `${namespace}JwtDecode` | ||
|  |     jwtVerifyName = options.jwtVerify || `${namespace}JwtVerify` | ||
|  |     jwtSignName = options.jwtSign || `${namespace}JwtSign` | ||
|  |   } else { | ||
|  |     fastify.decorateRequest('user', null) | ||
|  |     fastify.decorate('jwt', jwtConfig) | ||
|  |   } | ||
|  | 
 | ||
|  |   // Temporary conditional to prevent breaking changes by exposing `jwtDecode`,
 | ||
|  |   // which already exists in fastify-auth0-verify.
 | ||
|  |   // If jwtDecode has been requested, or plugin is configured to use a namespace.
 | ||
|  |   // TODO Remove conditional when fastify-jwt >=4.x.x
 | ||
|  |   if (options.jwtDecode || namespace) { | ||
|  |     fastify.decorateRequest(jwtDecodeName, requestDecode) | ||
|  |   } | ||
|  |   fastify.decorateRequest(jwtVerifyName, requestVerify) | ||
|  |   fastify.decorateReply(jwtSignName, replySign) | ||
|  | 
 | ||
|  |   const signerConfig = checkAndMergeSignOptions() | ||
|  |   const signer = createSigner(signerConfig.options) | ||
|  |   const decoder = createDecoder(decodeOptions) | ||
|  |   const verifierConfig = checkAndMergeVerifyOptions() | ||
|  |   const verifier = createVerifier(verifierConfig.options) | ||
|  | 
 | ||
|  |   next() | ||
|  | 
 | ||
|  |   function decode (token, options) { | ||
|  |     assert(token, 'missing token') | ||
|  | 
 | ||
|  |     if (options && typeof options !== 'function') { | ||
|  |       const localDecoder = createDecoder(options) | ||
|  |       return localDecoder(token) | ||
|  |     } | ||
|  | 
 | ||
|  |     return decoder(token) | ||
|  |   } | ||
|  | 
 | ||
|  |   function lookupToken (request, options) { | ||
|  |     assert(request, 'missing request') | ||
|  | 
 | ||
|  |     options = Object.assign({}, verifyOptions, options) | ||
|  | 
 | ||
|  |     let token | ||
|  |     const extractToken = options.extractToken | ||
|  |     if (extractToken) { | ||
|  |       token = extractToken(request) | ||
|  |       if (!token) { | ||
|  |         throw 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] | ||
|  |         token = parts[1] | ||
|  | 
 | ||
|  |         if (!/^Bearer$/i.test(scheme)) { | ||
|  |           throw new BadRequest(messagesOptions.badRequestErrorMessage) | ||
|  |         } | ||
|  |       } else { | ||
|  |         throw new BadRequest(messagesOptions.badRequestErrorMessage) | ||
|  |       } | ||
|  |     } else if (cookie) { | ||
|  |       if (request.cookies) { | ||
|  |         if (request.cookies[cookie.cookieName]) { | ||
|  |           const tokenValue = request.cookies[cookie.cookieName] | ||
|  | 
 | ||
|  |           token = cookie.signed ? request.unsignCookie(tokenValue).value : tokenValue | ||
|  |         } else { | ||
|  |           throw new Unauthorized(messagesOptions.noAuthorizationInCookieMessage) | ||
|  |         } | ||
|  |       } else { | ||
|  |         throw new BadRequest(messagesOptions.badCookieRequestErrorMessage) | ||
|  |       } | ||
|  |     } else { | ||
|  |       throw new Unauthorized(messagesOptions.noAuthorizationInHeaderMessage) | ||
|  |     } | ||
|  | 
 | ||
|  |     return token | ||
|  |   } | ||
|  | 
 | ||
|  |   function mergeOptionsWithKey (options, useProvidedPrivateKey) { | ||
|  |     if (useProvidedPrivateKey && (typeof useProvidedPrivateKey !== 'boolean')) { | ||
|  |       return Object.assign({}, options, { key: useProvidedPrivateKey }) | ||
|  |     } else { | ||
|  |       const key = useProvidedPrivateKey ? secretOrPrivateKey : secretOrPublicKey | ||
|  |       return Object.assign(!options.key ? { key } : {}, options) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function checkAndMergeOptions (options, defaultOptions, usePrivateKey, callback) { | ||
|  |     let mergedOptions | ||
|  | 
 | ||
|  |     if (typeof options === 'function') { | ||
|  |       callback = options | ||
|  |       mergedOptions = mergeOptionsWithKey(defaultOptions, usePrivateKey) | ||
|  |     } else { | ||
|  |       if (!options) { | ||
|  |         mergedOptions = mergeOptionsWithKey(defaultOptions, usePrivateKey) | ||
|  |       } else { | ||
|  |         mergedOptions = mergeOptionsWithKey(options, usePrivateKey) | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     return { options: mergedOptions, callback } | ||
|  |   } | ||
|  | 
 | ||
|  |   function checkAndMergeSignOptions (options, callback) { | ||
|  |     return checkAndMergeOptions(options, signOptions, true, callback) | ||
|  |   } | ||
|  | 
 | ||
|  |   function checkAndMergeVerifyOptions (options, callback) { | ||
|  |     return checkAndMergeOptions(options, verifyOptions, false, callback) | ||
|  |   } | ||
|  | 
 | ||
|  |   function sign (payload, options, callback) { | ||
|  |     assert(payload, 'missing payload') | ||
|  |     let localSigner = signer | ||
|  | 
 | ||
|  |     convertTemporalProps(options) | ||
|  |     const signerConfig = checkAndMergeSignOptions(options, callback) | ||
|  | 
 | ||
|  |     if (options && typeof options !== 'function') { | ||
|  |       localSigner = createSigner(signerConfig.options) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (typeof signerConfig.callback === 'function') { | ||
|  |       const token = localSigner(payload) | ||
|  |       signerConfig.callback(null, token) | ||
|  |     } else { | ||
|  |       return localSigner(payload) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function verify (token, options, callback) { | ||
|  |     assert(token, 'missing token') | ||
|  |     assert(secretOrPublicKey, 'missing secret') | ||
|  | 
 | ||
|  |     let localVerifier = verifier | ||
|  | 
 | ||
|  |     convertTemporalProps(options, true) | ||
|  |     const veriferConfig = checkAndMergeVerifyOptions(options, callback) | ||
|  | 
 | ||
|  |     if (options && typeof options !== 'function') { | ||
|  |       localVerifier = createVerifier(veriferConfig.options) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (typeof veriferConfig.callback === 'function') { | ||
|  |       const result = localVerifier(token) | ||
|  |       veriferConfig.callback(null, result) | ||
|  |     } else { | ||
|  |       return localVerifier(token) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function replySign (payload, options, next) { | ||
|  |     let useLocalSigner = true | ||
|  |     if (typeof options === 'function') { | ||
|  |       next = options | ||
|  |       options = {} | ||
|  |       useLocalSigner = false | ||
|  |     } // support no options
 | ||
|  | 
 | ||
|  |     if (!options) { | ||
|  |       options = {} | ||
|  |       useLocalSigner = false | ||
|  |     } | ||
|  | 
 | ||
|  |     const reply = this | ||
|  | 
 | ||
|  |     if (next === undefined) { | ||
|  |       return new Promise(function (resolve, reject) { | ||
|  |         reply[jwtSignName](payload, options, function (err, val) { | ||
|  |           err ? reject(err) : resolve(val) | ||
|  |         }) | ||
|  |       }) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (options.sign) { | ||
|  |       convertTemporalProps(options.sign) | ||
|  |       // New supported contract, options supports sign and can expand
 | ||
|  |       options = { | ||
|  |         sign: mergeOptionsWithKey({ ...signOptions, ...options.sign }, true) | ||
|  |       } | ||
|  |     } else { | ||
|  |       convertTemporalProps(options) | ||
|  |       // Original contract, options supports only sign
 | ||
|  |       options = mergeOptionsWithKey({ ...signOptions, ...options }, true) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!payload) { | ||
|  |       return next(new Error('jwtSign requires a payload')) | ||
|  |     } | ||
|  | 
 | ||
|  |     steed.waterfall([ | ||
|  |       function getSecret (callback) { | ||
|  |         const signResult = secretCallbackSign(reply.request, payload, callback) | ||
|  | 
 | ||
|  |         if (signResult && typeof signResult.then === 'function') { | ||
|  |           signResult.then(result => callback(null, result), callback) | ||
|  |         } | ||
|  |       }, | ||
|  |       function sign (secretOrPrivateKey, callback) { | ||
|  |         if (useLocalSigner) { | ||
|  |           const signerOptions = mergeOptionsWithKey(options.sign || options, secretOrPrivateKey) | ||
|  |           const localSigner = createSigner(signerOptions) | ||
|  |           const token = localSigner(payload) | ||
|  |           callback(null, token) | ||
|  |         } else { | ||
|  |           const token = signer(payload) | ||
|  |           callback(null, token) | ||
|  |         } | ||
|  |       } | ||
|  |     ], next) | ||
|  |   } | ||
|  | 
 | ||
|  |   function requestDecode (options, next) { | ||
|  |     if (typeof options === 'function' && !next) { | ||
|  |       next = options | ||
|  |       options = {} | ||
|  |     } // support no options
 | ||
|  | 
 | ||
|  |     if (!options) { | ||
|  |       options = {} | ||
|  |     } | ||
|  | 
 | ||
|  |     options = { | ||
|  |       decode: Object.assign({}, decodeOptions, options.decode), | ||
|  |       verify: Object.assign({}, verifyOptions, options.verify) | ||
|  |     } | ||
|  | 
 | ||
|  |     const request = this | ||
|  | 
 | ||
|  |     if (next === undefined) { | ||
|  |       return new Promise(function (resolve, reject) { | ||
|  |         request[jwtDecodeName](options, function (err, val) { | ||
|  |           err ? reject(err) : resolve(val) | ||
|  |         }) | ||
|  |       }) | ||
|  |     } | ||
|  | 
 | ||
|  |     try { | ||
|  |       const token = lookupToken(request, options.verify) | ||
|  |       const decodedToken = decode(token, options.decode) | ||
|  |       return next(null, decodedToken) | ||
|  |     } catch (err) { | ||
|  |       return next(err) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function requestVerify (options, next) { | ||
|  |     let useLocalVerifier = true | ||
|  |     if (typeof options === 'function' && !next) { | ||
|  |       next = options | ||
|  |       options = {} | ||
|  |       useLocalVerifier = false | ||
|  |     } // support no options
 | ||
|  | 
 | ||
|  |     if (!options) { | ||
|  |       options = {} | ||
|  |       useLocalVerifier = false | ||
|  |     } | ||
|  | 
 | ||
|  |     if (options.decode || options.verify) { | ||
|  |       convertTemporalProps(options.verify, true) | ||
|  |       // New supported contract, options supports both decode and verify
 | ||
|  |       options = { | ||
|  |         decode: Object.assign({}, decodeOptions, options.decode), | ||
|  |         verify: Object.assign({}, verifyOptions, options.verify) | ||
|  |       } | ||
|  |     } else { | ||
|  |       convertTemporalProps(options, true) | ||
|  |       // Original contract, options supports only verify
 | ||
|  |       options = Object.assign({}, verifyOptions, options) | ||
|  |     } | ||
|  | 
 | ||
|  |     const request = this | ||
|  | 
 | ||
|  |     if (next === undefined) { | ||
|  |       return new Promise(function (resolve, reject) { | ||
|  |         request[jwtVerifyName](options, function (err, val) { | ||
|  |           err ? reject(err) : resolve(val) | ||
|  |         }) | ||
|  |       }) | ||
|  |     } | ||
|  | 
 | ||
|  |     let token | ||
|  |     try { | ||
|  |       token = lookupToken(request, options.verify || options) | ||
|  |     } catch (err) { | ||
|  |       return next(err) | ||
|  |     } | ||
|  | 
 | ||
|  |     const decodedToken = decode(token, options.decode || decodeOptions) | ||
|  | 
 | ||
|  |     steed.waterfall([ | ||
|  |       function getSecret (callback) { | ||
|  |         const verifyResult = secretCallbackVerify(request, decodedToken, callback) | ||
|  |         if (verifyResult && typeof verifyResult.then === 'function') { | ||
|  |           verifyResult.then(result => callback(null, result), callback) | ||
|  |         } | ||
|  |       }, | ||
|  |       function verify (secretOrPublicKey, callback) { | ||
|  |         try { | ||
|  |           if (useLocalVerifier) { | ||
|  |             const verifierOptions = mergeOptionsWithKey(options.verify || options, secretOrPublicKey) | ||
|  |             const localVerifier = createVerifier(verifierOptions) | ||
|  |             const verifyResult = localVerifier(token) | ||
|  |             callback(null, verifyResult) | ||
|  |           } else { | ||
|  |             const verifyResult = verifier(token) | ||
|  |             callback(null, verifyResult) | ||
|  |           } | ||
|  |         } catch (error) { | ||
|  |           if (error.code === TokenError.codes.expired) { | ||
|  |             return callback(new Unauthorized(messagesOptions.authorizationTokenExpiredMessage)) | ||
|  |           } | ||
|  | 
 | ||
|  |           if (error.code === TokenError.codes.invalidKey || | ||
|  |               error.code === TokenError.codes.invalidSignature || | ||
|  |               error.code === TokenError.codes.invalidClaimValue | ||
|  |           ) { | ||
|  |             return callback(new Unauthorized(typeof messagesOptions.authorizationTokenInvalid === 'function' ? messagesOptions.authorizationTokenInvalid(error) : messagesOptions.authorizationTokenInvalid)) | ||
|  |           } | ||
|  | 
 | ||
|  |           return callback(error) | ||
|  |         } | ||
|  |       }, | ||
|  |       function checkIfIsTrusted (result, callback) { | ||
|  |         if (!trusted) { | ||
|  |           callback(null, result) | ||
|  |         } else { | ||
|  |           const maybePromise = trusted(request, result) | ||
|  | 
 | ||
|  |           if (maybePromise && maybePromise.then) { | ||
|  |             maybePromise | ||
|  |               .then(trusted => trusted ? callback(null, result) : callback(new Unauthorized(messagesOptions.authorizationTokenUntrusted))) | ||
|  |           } else if (maybePromise) { | ||
|  |             callback(null, maybePromise) | ||
|  |           } else { | ||
|  |             callback(new Unauthorized(messagesOptions.authorizationTokenUntrusted)) | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     ], function (err, result) { | ||
|  |       if (err) { | ||
|  |         next(err) | ||
|  |       } else { | ||
|  |         const user = formatUser ? formatUser(result) : result | ||
|  |         request.user = user | ||
|  |         next(null, user) | ||
|  |       } | ||
|  |     }) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = fp(fastifyJwt, { | ||
|  |   fastify: '>=3.0.0', | ||
|  |   name: 'fastify-jwt' | ||
|  | }) |