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.
		
		
		
		
		
			
		
			
				
					310 lines
				
				8.9 KiB
			
		
		
			
		
	
	
					310 lines
				
				8.9 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								'use strict'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const {
							 | 
						||
| 
								 | 
							
								  base64UrlMatcher,
							 | 
						||
| 
								 | 
							
								  base64UrlReplacer,
							 | 
						||
| 
								 | 
							
								  useNewCrypto,
							 | 
						||
| 
								 | 
							
								  hsAlgorithms,
							 | 
						||
| 
								 | 
							
								  esAlgorithms,
							 | 
						||
| 
								 | 
							
								  rsaAlgorithms,
							 | 
						||
| 
								 | 
							
								  edAlgorithms,
							 | 
						||
| 
								 | 
							
								  detectPrivateKeyAlgorithm,
							 | 
						||
| 
								 | 
							
								  createSignature
							 | 
						||
| 
								 | 
							
								} = require('./crypto')
							 | 
						||
| 
								 | 
							
								const { TokenError } = require('./error')
							 | 
						||
| 
								 | 
							
								const { getAsyncKey, ensurePromiseCallback } = require('./utils')
							 | 
						||
| 
								 | 
							
								const { createPrivateKey, createSecretKey } = require('crypto')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const supportedAlgorithms = Array.from(
							 | 
						||
| 
								 | 
							
								  new Set([...hsAlgorithms, ...esAlgorithms, ...rsaAlgorithms, ...edAlgorithms, 'none'])
							 | 
						||
| 
								 | 
							
								).join(', ')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function checkIsCompatibleAlgorithm(expected, actual) {
							 | 
						||
| 
								 | 
							
								  const expectedType = expected.slice(0, 2)
							 | 
						||
| 
								 | 
							
								  const actualType = actual.slice(0, 2)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let valid = true // We accept everything for HS
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // If the key is passphrase encrypted (actual === "ENCRYPTED") only RS and ES algos are supported
							 | 
						||
| 
								 | 
							
								  if (expectedType === 'RS' || expectedType === 'PS') {
							 | 
						||
| 
								 | 
							
								    // RS and PS use same keys
							 | 
						||
| 
								 | 
							
								    valid = actualType === 'RS' || (expectedType === 'RS' && actual === 'ENCRYPTED')
							 | 
						||
| 
								 | 
							
								  } else if (expectedType === 'ES' || expectedType === 'Ed') {
							 | 
						||
| 
								 | 
							
								    // ES and Ed must match
							 | 
						||
| 
								 | 
							
								    valid = expectedType === actualType || (expectedType === 'ES' && actual === 'ENCRYPTED')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!valid) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidKey, `Invalid private key provided for algorithm ${expected}.`)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function prepareKeyOrSecret(key, algorithm) {
							 | 
						||
| 
								 | 
							
								  if (typeof key === 'string') {
							 | 
						||
| 
								 | 
							
								    key = Buffer.from(key, 'utf-8')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Only on Node 12 - Create a key object
							 | 
						||
| 
								 | 
							
								  /* istanbul ignore next */
							 | 
						||
| 
								 | 
							
								  if (useNewCrypto) {
							 | 
						||
| 
								 | 
							
								    key = algorithm[0] === 'H' ? createSecretKey(key) : createPrivateKey(key)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return key
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function sign(
							 | 
						||
| 
								 | 
							
								  {
							 | 
						||
| 
								 | 
							
								    key,
							 | 
						||
| 
								 | 
							
								    algorithm,
							 | 
						||
| 
								 | 
							
								    noTimestamp,
							 | 
						||
| 
								 | 
							
								    mutatePayload,
							 | 
						||
| 
								 | 
							
								    clockTimestamp,
							 | 
						||
| 
								 | 
							
								    expiresIn,
							 | 
						||
| 
								 | 
							
								    notBefore,
							 | 
						||
| 
								 | 
							
								    kid,
							 | 
						||
| 
								 | 
							
								    typ,
							 | 
						||
| 
								 | 
							
								    isAsync,
							 | 
						||
| 
								 | 
							
								    additionalHeader,
							 | 
						||
| 
								 | 
							
								    fixedPayload
							 | 
						||
| 
								 | 
							
								  },
							 | 
						||
| 
								 | 
							
								  payload,
							 | 
						||
| 
								 | 
							
								  cb
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
								  const [callback, promise] = isAsync ? ensurePromiseCallback(cb) : []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Validate payload
							 | 
						||
| 
								 | 
							
								  if (typeof payload !== 'object') {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidType, 'The payload must be an object.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (payload.exp && (!Number.isInteger(payload.exp) || payload.exp < 0)) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidClaimValue, 'The exp claim must be a positive integer.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Prepare the header
							 | 
						||
| 
								 | 
							
								  const header = {
							 | 
						||
| 
								 | 
							
								    alg: algorithm,
							 | 
						||
| 
								 | 
							
								    typ: typ || 'JWT',
							 | 
						||
| 
								 | 
							
								    kid,
							 | 
						||
| 
								 | 
							
								    ...additionalHeader
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Prepare the payload
							 | 
						||
| 
								 | 
							
								  let encodedPayload = ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Add claims
							 | 
						||
| 
								 | 
							
								  const iat = payload.iat * 1000 || clockTimestamp || Date.now()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const finalPayload = {
							 | 
						||
| 
								 | 
							
								    ...payload,
							 | 
						||
| 
								 | 
							
								    ...fixedPayload,
							 | 
						||
| 
								 | 
							
								    iat: noTimestamp ? undefined : Math.floor(iat / 1000),
							 | 
						||
| 
								 | 
							
								    exp: payload.exp ? payload.exp : expiresIn ? Math.floor((iat + expiresIn) / 1000) : undefined,
							 | 
						||
| 
								 | 
							
								    nbf: notBefore ? Math.floor((iat + notBefore) / 1000) : undefined
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (mutatePayload) {
							 | 
						||
| 
								 | 
							
								    Object.assign(payload, finalPayload)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  encodedPayload = Buffer.from(JSON.stringify(finalPayload), 'utf-8')
							 | 
						||
| 
								 | 
							
								    .toString('base64')
							 | 
						||
| 
								 | 
							
								    .replace(base64UrlMatcher, base64UrlReplacer)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // We have the key
							 | 
						||
| 
								 | 
							
								  if (!callback) {
							 | 
						||
| 
								 | 
							
								    const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8')
							 | 
						||
| 
								 | 
							
								      .toString('base64')
							 | 
						||
| 
								 | 
							
								      .replace(base64UrlMatcher, base64UrlReplacer)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const input = encodedHeader + '.' + encodedPayload
							 | 
						||
| 
								 | 
							
								    const signature = algorithm === 'none' ? '' : createSignature(algorithm, key, input)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return input + '.' + signature
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Get the key asynchronously
							 | 
						||
| 
								 | 
							
								  getAsyncKey(key, header, (err, currentKey) => {
							 | 
						||
| 
								 | 
							
								    if (err) {
							 | 
						||
| 
								 | 
							
								      const error = TokenError.wrap(err, TokenError.codes.keyFetchingError, 'Cannot fetch key.')
							 | 
						||
| 
								 | 
							
								      return callback(error)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (typeof currentKey === 'string') {
							 | 
						||
| 
								 | 
							
								      currentKey = Buffer.from(currentKey, 'utf-8')
							 | 
						||
| 
								 | 
							
								    } else if (!(currentKey instanceof Buffer)) {
							 | 
						||
| 
								 | 
							
								      return callback(
							 | 
						||
| 
								 | 
							
								        new TokenError(
							 | 
						||
| 
								 | 
							
								          TokenError.codes.keyFetchingError,
							 | 
						||
| 
								 | 
							
								          'The key returned from the callback must be a string or a buffer containing a secret or a private key.'
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								      )
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    let token
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								      // Detect the private key - If the algorithm was known, just verify they match, otherwise assign it
							 | 
						||
| 
								 | 
							
								      const availableAlgorithm = detectPrivateKeyAlgorithm(currentKey, algorithm)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (algorithm) {
							 | 
						||
| 
								 | 
							
								        checkIsCompatibleAlgorithm(algorithm, availableAlgorithm)
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        header.alg = algorithm = availableAlgorithm
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      currentKey = prepareKeyOrSecret(currentKey, algorithm)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8')
							 | 
						||
| 
								 | 
							
								        .toString('base64')
							 | 
						||
| 
								 | 
							
								        .replace(base64UrlMatcher, base64UrlReplacer)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      const input = encodedHeader + '.' + encodedPayload
							 | 
						||
| 
								 | 
							
								      token = input + '.' + createSignature(algorithm, currentKey, input)
							 | 
						||
| 
								 | 
							
								    } catch (e) {
							 | 
						||
| 
								 | 
							
								      return callback(e)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    callback(null, token)
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return promise
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = function createSigner(options) {
							 | 
						||
| 
								 | 
							
								  let {
							 | 
						||
| 
								 | 
							
								    key,
							 | 
						||
| 
								 | 
							
								    algorithm,
							 | 
						||
| 
								 | 
							
								    noTimestamp,
							 | 
						||
| 
								 | 
							
								    mutatePayload,
							 | 
						||
| 
								 | 
							
								    clockTimestamp,
							 | 
						||
| 
								 | 
							
								    expiresIn,
							 | 
						||
| 
								 | 
							
								    notBefore,
							 | 
						||
| 
								 | 
							
								    jti,
							 | 
						||
| 
								 | 
							
								    aud,
							 | 
						||
| 
								 | 
							
								    iss,
							 | 
						||
| 
								 | 
							
								    sub,
							 | 
						||
| 
								 | 
							
								    nonce,
							 | 
						||
| 
								 | 
							
								    kid,
							 | 
						||
| 
								 | 
							
								    typ,
							 | 
						||
| 
								 | 
							
								    header: additionalHeader
							 | 
						||
| 
								 | 
							
								  } = { clockTimestamp: 0, ...options }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Validate options
							 | 
						||
| 
								 | 
							
								  if (
							 | 
						||
| 
								 | 
							
								    algorithm &&
							 | 
						||
| 
								 | 
							
								    algorithm !== 'none' &&
							 | 
						||
| 
								 | 
							
								    !hsAlgorithms.includes(algorithm) &&
							 | 
						||
| 
								 | 
							
								    !esAlgorithms.includes(algorithm) &&
							 | 
						||
| 
								 | 
							
								    !rsaAlgorithms.includes(algorithm) &&
							 | 
						||
| 
								 | 
							
								    !edAlgorithms.includes(algorithm)
							 | 
						||
| 
								 | 
							
								  ) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(
							 | 
						||
| 
								 | 
							
								      TokenError.codes.invalidOption,
							 | 
						||
| 
								 | 
							
								      `The algorithm option must be one of the following values: ${supportedAlgorithms}.`
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const keyType = typeof key
							 | 
						||
| 
								 | 
							
								  const isKeyPasswordProtected = keyType === 'object' && key && key.key && key.passphrase
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (algorithm === 'none') {
							 | 
						||
| 
								 | 
							
								    if (key) {
							 | 
						||
| 
								 | 
							
								      throw new TokenError(
							 | 
						||
| 
								 | 
							
								        TokenError.codes.invalidOption,
							 | 
						||
| 
								 | 
							
								        'The key option must not be provided when the algorithm option is "none".'
							 | 
						||
| 
								 | 
							
								      )
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  } else if (
							 | 
						||
| 
								 | 
							
								    !key ||
							 | 
						||
| 
								 | 
							
								    (keyType !== 'string' && !(key instanceof Buffer) && keyType !== 'function' && !isKeyPasswordProtected)
							 | 
						||
| 
								 | 
							
								  ) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(
							 | 
						||
| 
								 | 
							
								      TokenError.codes.invalidOption,
							 | 
						||
| 
								 | 
							
								      'The key option must be a string, a buffer, an object containing key/passphrase properties or a function returning the algorithm secret or private key.'
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								  } else if (isKeyPasswordProtected && !algorithm) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(
							 | 
						||
| 
								 | 
							
								      TokenError.codes.invalidAlgorithm,
							 | 
						||
| 
								 | 
							
								      'When using password protected key you must provide the algorithm option.'
							 | 
						||
| 
								 | 
							
								    )
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Convert the key to a string when not a function, in order to be able to detect
							 | 
						||
| 
								 | 
							
								  if (key && keyType !== 'function') {
							 | 
						||
| 
								 | 
							
								    // Detect the private key - If the algorithm was known, just verify they match, otherwise assign it
							 | 
						||
| 
								 | 
							
								    const availableAlgorithm = detectPrivateKeyAlgorithm(isKeyPasswordProtected ? key.key : key, algorithm)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (algorithm) {
							 | 
						||
| 
								 | 
							
								      checkIsCompatibleAlgorithm(algorithm, availableAlgorithm)
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      algorithm = availableAlgorithm
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    key = prepareKeyOrSecret(key, algorithm)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (expiresIn && (typeof expiresIn !== 'number' || expiresIn < 0)) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The expiresIn option must be a positive number.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (notBefore && (typeof notBefore !== 'number' || notBefore < 0)) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The notBefore option must be a positive number.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (clockTimestamp && (typeof clockTimestamp !== 'number' || clockTimestamp < 0)) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The clockTimestamp option must be a positive number.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (jti && typeof jti !== 'string') {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The jti option must be a string.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (aud && typeof aud !== 'string' && !Array.isArray(aud)) {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The aud option must be a string or an array of strings.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (iss && typeof iss !== 'string') {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The iss option must be a string.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (sub && typeof sub !== 'string') {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The sub option must be a string.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (nonce && typeof nonce !== 'string') {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The nonce option must be a string.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (kid && typeof kid !== 'string') {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The kid option must be a string.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (additionalHeader && typeof additionalHeader !== 'object') {
							 | 
						||
| 
								 | 
							
								    throw new TokenError(TokenError.codes.invalidOption, 'The header option must be a object.')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const fpo = { jti, aud, iss, sub, nonce }
							 | 
						||
| 
								 | 
							
								  const fixedPayload = Object.keys(fpo).reduce((obj, key) => {
							 | 
						||
| 
								 | 
							
								    return fpo[key] !== undefined ? Object.assign(obj, { [key]: fpo[key] }) : obj
							 | 
						||
| 
								 | 
							
								  }, {})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Return the signer
							 | 
						||
| 
								 | 
							
								  const context = {
							 | 
						||
| 
								 | 
							
								    key,
							 | 
						||
| 
								 | 
							
								    algorithm,
							 | 
						||
| 
								 | 
							
								    noTimestamp,
							 | 
						||
| 
								 | 
							
								    mutatePayload,
							 | 
						||
| 
								 | 
							
								    clockTimestamp,
							 | 
						||
| 
								 | 
							
								    expiresIn,
							 | 
						||
| 
								 | 
							
								    notBefore,
							 | 
						||
| 
								 | 
							
								    kid,
							 | 
						||
| 
								 | 
							
								    typ,
							 | 
						||
| 
								 | 
							
								    isAsync: keyType === 'function',
							 | 
						||
| 
								 | 
							
								    additionalHeader,
							 | 
						||
| 
								 | 
							
								    fixedPayload
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return sign.bind(null, context)
							 | 
						||
| 
								 | 
							
								}
							 |