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
						
					
					
				'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'
 | 
						|
})
 |