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.
		
		
		
		
		
			
		
			
				
					
					
						
							524 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
	
	
							524 lines
						
					
					
						
							16 KiB
						
					
					
				| '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 createError = require('@fastify/error')
 | |
| 
 | |
| 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') {
 | |
|     return options
 | |
|   }
 | |
| 
 | |
|   const formatedOptions = Object.assign({}, options)
 | |
| 
 | |
|   if (isVerifyOptions && formatedOptions.maxAge) {
 | |
|     formatedOptions.maxAge = convertToMs(formatedOptions.maxAge)
 | |
|   } else if (formatedOptions.expiresIn || formatedOptions.notBefore) {
 | |
|     if (formatedOptions.expiresIn) {
 | |
|       formatedOptions.expiresIn = convertToMs(formatedOptions.expiresIn)
 | |
|     }
 | |
| 
 | |
|     if (formatedOptions.notBefore) {
 | |
|       formatedOptions.notBefore = convertToMs(formatedOptions.notBefore)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return formatedOptions
 | |
| }
 | |
| 
 | |
| 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 {
 | |
|     cookie,
 | |
|     decode: decodeOptions = {},
 | |
|     formatUser,
 | |
|     jwtDecode,
 | |
|     jwtSign,
 | |
|     jwtVerify,
 | |
|     secret,
 | |
|     sign: initialSignOptions = {},
 | |
|     trusted,
 | |
|     decoratorName = 'user',
 | |
|     // TODO: disable on next major
 | |
|     // enable errorCacheTTL to prevent breaking change
 | |
|     verify: initialVerifyOptions = { errorCacheTTL: 600000 },
 | |
|     ...pluginOptions
 | |
|   } = options
 | |
| 
 | |
|   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 signOptions = convertTemporalProps(initialSignOptions)
 | |
|   const verifyOptions = convertTemporalProps(initialVerifyOptions, true)
 | |
|   const messagesOptions = Object.assign({}, messages, pluginOptions.messages)
 | |
|   const namespace = typeof pluginOptions.namespace === 'string' ? pluginOptions.namespace : undefined
 | |
| 
 | |
|   const NoAuthorizationInCookieError = createError('FST_JWT_NO_AUTHORIZATION_IN_COOKIE', messagesOptions.noAuthorizationInCookieMessage, 401)
 | |
|   const AuthorizationTokenExpiredError = createError('FST_JWT_AUTHORIZATION_TOKEN_EXPIRED', messagesOptions.authorizationTokenExpiredMessage, 401)
 | |
|   const AuthorizationTokenUntrustedError = createError('FST_JWT_AUTHORIZATION_TOKEN_UNTRUSTED', messagesOptions.authorizationTokenUntrusted, 401)
 | |
|   const NoAuthorizationInHeaderError = createError('FST_JWT_NO_AUTHORIZATION_IN_HEADER', messagesOptions.noAuthorizationInHeaderMessage, 401)
 | |
|   const AuthorizationTokenInvalidError = createError('FST_JWT_AUTHORIZATION_TOKEN_INVALID', typeof messagesOptions.authorizationTokenInvalid === 'function'
 | |
|     ? messagesOptions.authorizationTokenInvalid({ message: '%s' })
 | |
|     : messagesOptions.authorizationTokenInvalid
 | |
|   , 401)
 | |
|   const BadRequestError = createError('FST_JWT_BAD_REQUEST', messagesOptions.badRequestErrorMessage, 400)
 | |
|   const BadCookieRequestError = createError('FST_JWT_BAD_COOKIE_REQUEST', messagesOptions.badCookieRequestErrorMessage, 400)
 | |
| 
 | |
|   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 jwtDecorator = {
 | |
|     decode,
 | |
|     options: {
 | |
|       decode: decodeOptions,
 | |
|       sign: initialSignOptions,
 | |
|       verify: initialVerifyOptions,
 | |
|       messages: messagesOptions,
 | |
|       decoratorName
 | |
|     },
 | |
|     cookie,
 | |
|     sign,
 | |
|     verify,
 | |
|     lookupToken
 | |
|   }
 | |
| 
 | |
|   let jwtDecodeName = 'jwtDecode'
 | |
|   let jwtVerifyName = 'jwtVerify'
 | |
|   let jwtSignName = 'jwtSign'
 | |
|   if (namespace) {
 | |
|     if (!fastify.jwt) {
 | |
|       fastify.decorateRequest(decoratorName, null)
 | |
|       fastify.decorate('jwt', Object.create(null))
 | |
|     }
 | |
| 
 | |
|     if (fastify.jwt[namespace]) {
 | |
|       return next(new Error(`JWT namespace already used "${namespace}"`))
 | |
|     }
 | |
|     fastify.jwt[namespace] = jwtDecorator
 | |
| 
 | |
|     jwtDecodeName = jwtDecode ? (typeof jwtDecode === 'string' ? jwtDecode : 'jwtDecode') : `${namespace}JwtDecode`
 | |
|     jwtVerifyName = jwtVerify || `${namespace}JwtVerify`
 | |
|     jwtSignName = jwtSign || `${namespace}JwtSign`
 | |
|   } else {
 | |
|     fastify.decorateRequest(decoratorName, null)
 | |
|     fastify.decorate('jwt', jwtDecorator)
 | |
|   }
 | |
| 
 | |
|   // 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 (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
 | |
|     const onlyCookie = options.onlyCookie
 | |
|     if (extractToken) {
 | |
|       token = extractToken(request)
 | |
|       if (!token) {
 | |
|         throw new BadRequestError()
 | |
|       }
 | |
|     } else if ((request.headers && request.headers.authorization) && (!onlyCookie)) {
 | |
|       const parts = request.headers.authorization.split(' ')
 | |
|       if (parts.length === 2) {
 | |
|         const scheme = parts[0]
 | |
|         token = parts[1]
 | |
| 
 | |
|         if (!/^Bearer$/i.test(scheme)) {
 | |
|           throw new BadRequestError()
 | |
|         }
 | |
|       } else {
 | |
|         throw new BadRequestError()
 | |
|       }
 | |
|     } 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 NoAuthorizationInCookieError()
 | |
|         }
 | |
|       } else {
 | |
|         throw new BadCookieRequestError()
 | |
|       }
 | |
|     } else {
 | |
|       throw new NoAuthorizationInHeaderError()
 | |
|     }
 | |
| 
 | |
|     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) {
 | |
|     if (typeof options === 'function') {
 | |
|       return { options: mergeOptionsWithKey(defaultOptions, usePrivateKey), callback: options }
 | |
|     }
 | |
| 
 | |
|     return { options: mergeOptionsWithKey(options || defaultOptions, usePrivateKey), 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
 | |
| 
 | |
|     const localOptions = convertTemporalProps(options)
 | |
|     const signerConfig = checkAndMergeSignOptions(localOptions, 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
 | |
| 
 | |
|     const localOptions = convertTemporalProps(options, true)
 | |
|     const veriferConfig = checkAndMergeVerifyOptions(localOptions, 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) {
 | |
|       const localSignOptions = convertTemporalProps(options.sign)
 | |
|       // New supported contract, options supports sign and can expand
 | |
|       options = {
 | |
|         sign: mergeOptionsWithKey(Object.assign(signOptions, localSignOptions), true)
 | |
|       }
 | |
|     } else {
 | |
|       const localOptions = convertTemporalProps(options)
 | |
|       // Original contract, options supports only sign
 | |
|       options = mergeOptionsWithKey(Object.assign(signOptions, localOptions), 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) {
 | |
|       const localVerifyOptions = convertTemporalProps(options.verify, true)
 | |
|       // New supported contract, options supports both decode and verify
 | |
|       options = {
 | |
|         decode: Object.assign({}, decodeOptions, options.decode),
 | |
|         verify: Object.assign({}, verifyOptions, localVerifyOptions)
 | |
|       }
 | |
|     } else {
 | |
|       const localOptions = convertTemporalProps(options, true)
 | |
|       // Original contract, options supports only verify
 | |
|       options = Object.assign({}, verifyOptions, localOptions)
 | |
|     }
 | |
| 
 | |
|     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 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())
 | |
|           }
 | |
| 
 | |
|           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 AuthorizationTokenUntrustedError()))
 | |
|           } else if (maybePromise) {
 | |
|             callback(null, maybePromise)
 | |
|           } else {
 | |
|             callback(new AuthorizationTokenUntrustedError())
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     ], function (err, result) {
 | |
|       if (err) {
 | |
|         next(err)
 | |
|       } else {
 | |
|         const user = formatUser ? formatUser(result) : result
 | |
|         request[decoratorName] = user
 | |
|         next(null, user)
 | |
|       }
 | |
|     })
 | |
|   }
 | |
| }
 | |
| 
 | |
| module.exports = fp(fastifyJwt, {
 | |
|   fastify: '4.x',
 | |
|   name: '@fastify/jwt'
 | |
| })
 | |
| module.exports.default = fastifyJwt
 | |
| module.exports.fastifyJwt = fastifyJwt
 |