Projektstart
This commit is contained in:
327
backend/node_modules/fast-jwt/src/crypto.js
generated
vendored
Normal file
327
backend/node_modules/fast-jwt/src/crypto.js
generated
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
'use strict'
|
||||
|
||||
const asn = require('asn1.js')
|
||||
const {
|
||||
createHmac,
|
||||
createVerify,
|
||||
createSign,
|
||||
timingSafeEqual,
|
||||
createPublicKey,
|
||||
constants: {
|
||||
RSA_PKCS1_PSS_PADDING,
|
||||
RSA_PSS_SALTLEN_DIGEST,
|
||||
RSA_PKCS1_PADDING,
|
||||
RSA_PSS_SALTLEN_MAX_SIGN,
|
||||
RSA_PSS_SALTLEN_AUTO
|
||||
}
|
||||
} = require('node:crypto')
|
||||
const { sign: directSign, verify: directVerify } = require('node:crypto')
|
||||
const { joseToDer, derToJose } = require('ecdsa-sig-formatter')
|
||||
const Cache = require('mnemonist/lru-cache')
|
||||
const { TokenError } = require('./error')
|
||||
|
||||
const base64UrlMatcher = /[=+/]/g
|
||||
const encoderMap = { '=': '', '+': '-', '/': '_' }
|
||||
|
||||
const privateKeyPemMatcher = /^-----BEGIN(?: (RSA|EC|ENCRYPTED))? PRIVATE KEY-----/
|
||||
const publicKeyPemMatcher = /^-----BEGIN(?: (RSA))? PUBLIC KEY-----/
|
||||
const publicKeyX509CertMatcher = '-----BEGIN CERTIFICATE-----'
|
||||
const privateKeysCache = new Cache(1000)
|
||||
const publicKeysCache = new Cache(1000)
|
||||
|
||||
const hsAlgorithms = ['HS256', 'HS384', 'HS512']
|
||||
const esAlgorithms = ['ES256', 'ES384', 'ES512']
|
||||
const rsaAlgorithms = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512']
|
||||
const edAlgorithms = ['EdDSA']
|
||||
const ecCurves = {
|
||||
'1.2.840.10045.3.1.7': { bits: '256', names: ['P-256', 'prime256v1'] },
|
||||
'1.3.132.0.10': { bits: '256', names: ['secp256k1'] },
|
||||
'1.3.132.0.34': { bits: '384', names: ['P-384', 'secp384r1'] },
|
||||
'1.3.132.0.35': { bits: '512', names: ['P-521', 'secp521r1'] }
|
||||
}
|
||||
|
||||
const PrivateKey = asn.define('PrivateKey', function () {
|
||||
this.seq().obj(
|
||||
this.key('version').int(),
|
||||
this.key('algorithm').seq().obj(this.key('algorithm').objid(), this.key('parameters').optional().objid())
|
||||
)
|
||||
})
|
||||
|
||||
const PublicKey = asn.define('PublicKey', function () {
|
||||
this.seq().obj(
|
||||
this.key('algorithm').seq().obj(this.key('algorithm').objid(), this.key('parameters').optional().objid())
|
||||
)
|
||||
})
|
||||
|
||||
const ECPrivateKey = asn.define('ECPrivateKey', function () {
|
||||
this.seq().obj(
|
||||
this.key('version').int(),
|
||||
this.key('privateKey').octstr(),
|
||||
this.key('parameters').explicit(0).optional().choice({ namedCurve: this.objid() })
|
||||
)
|
||||
})
|
||||
|
||||
function base64UrlReplacer(c) {
|
||||
return encoderMap[c]
|
||||
}
|
||||
|
||||
function cacheSet(cache, key, value, error) {
|
||||
cache.set(key, [value, error])
|
||||
return value || error
|
||||
}
|
||||
|
||||
function performDetectPrivateKeyAlgorithm(key) {
|
||||
if (key.match(publicKeyPemMatcher) || key.includes(publicKeyX509CertMatcher)) {
|
||||
throw new TokenError(TokenError.codes.invalidKey, 'Public keys are not supported for signing.')
|
||||
}
|
||||
|
||||
const pemData = key.trim().match(privateKeyPemMatcher)
|
||||
|
||||
if (!pemData) {
|
||||
return 'HS256'
|
||||
}
|
||||
|
||||
let keyData
|
||||
let oid
|
||||
let curveId
|
||||
|
||||
switch (pemData[1]) {
|
||||
case 'RSA': // pkcs1 format - Can only be RSA key
|
||||
return 'RS256'
|
||||
case 'EC': // sec1 format - Can only be a EC key
|
||||
keyData = ECPrivateKey.decode(key, 'pem', { label: 'EC PRIVATE KEY' })
|
||||
curveId = keyData.parameters.value.join('.')
|
||||
break
|
||||
case 'ENCRYPTED': // Can be either RSA or EC key - we'll used the supplied algorithm
|
||||
return 'ENCRYPTED'
|
||||
default:
|
||||
// pkcs8
|
||||
keyData = PrivateKey.decode(key, 'pem', { label: 'PRIVATE KEY' })
|
||||
oid = keyData.algorithm.algorithm.join('.')
|
||||
|
||||
switch (oid) {
|
||||
case '1.2.840.113549.1.1.1': // RSA
|
||||
return 'RS256'
|
||||
case '1.2.840.10045.2.1': // EC
|
||||
curveId = keyData.algorithm.parameters.join('.')
|
||||
break
|
||||
case '1.3.101.112': // Ed25519
|
||||
case '1.3.101.113': // Ed448
|
||||
return 'EdDSA'
|
||||
default:
|
||||
throw new TokenError(TokenError.codes.invalidKey, `Unsupported PEM PCKS8 private key with OID ${oid}.`)
|
||||
}
|
||||
}
|
||||
|
||||
const curve = ecCurves[curveId]
|
||||
|
||||
if (!curve) {
|
||||
throw new TokenError(TokenError.codes.invalidKey, `Unsupported EC private key with curve ${curveId}.`)
|
||||
}
|
||||
|
||||
return `ES${curve.bits}`
|
||||
}
|
||||
|
||||
function performDetectPublicKeyAlgorithms(key) {
|
||||
const publicKeyPemMatch = key.match(publicKeyPemMatcher)
|
||||
|
||||
if (key.match(privateKeyPemMatcher)) {
|
||||
throw new TokenError(TokenError.codes.invalidKey, 'Private keys are not supported for verifying.')
|
||||
} else if (publicKeyPemMatch && publicKeyPemMatch[1] === 'RSA') {
|
||||
// pkcs1 format - Can only be RSA key
|
||||
return rsaAlgorithms
|
||||
} else if (!publicKeyPemMatch && !key.includes(publicKeyX509CertMatcher)) {
|
||||
// Not a PEM, assume a plain secret
|
||||
return hsAlgorithms
|
||||
}
|
||||
|
||||
// if the key is a X509 cert we need to convert it
|
||||
if (key.includes(publicKeyX509CertMatcher)) {
|
||||
key = createPublicKey(key).export({ type: 'spki', format: 'pem' })
|
||||
}
|
||||
|
||||
const keyData = PublicKey.decode(key, 'pem', { label: 'PUBLIC KEY' })
|
||||
const oid = keyData.algorithm.algorithm.join('.')
|
||||
let curveId
|
||||
|
||||
switch (oid) {
|
||||
case '1.2.840.113549.1.1.1': // RSA
|
||||
return rsaAlgorithms
|
||||
case '1.2.840.10045.2.1': // EC
|
||||
curveId = keyData.algorithm.parameters.join('.')
|
||||
break
|
||||
case '1.3.101.112': // Ed25519
|
||||
case '1.3.101.113': // Ed448
|
||||
return ['EdDSA']
|
||||
default:
|
||||
throw new TokenError(TokenError.codes.invalidKey, `Unsupported PEM PCKS8 public key with OID ${oid}.`)
|
||||
}
|
||||
|
||||
const curve = ecCurves[curveId]
|
||||
|
||||
if (!curve) {
|
||||
throw new TokenError(TokenError.codes.invalidKey, `Unsupported EC public key with curve ${curveId}.`)
|
||||
}
|
||||
|
||||
return [`ES${curve.bits}`]
|
||||
}
|
||||
|
||||
function detectPrivateKeyAlgorithm(key, providedAlgorithm) {
|
||||
if (key instanceof Buffer) {
|
||||
key = key.toString('utf-8')
|
||||
} else if (typeof key !== 'string') {
|
||||
throw new TokenError(TokenError.codes.invalidKey, 'The private key must be a string or a buffer.')
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
const [cached, error] = privateKeysCache.get(key) || []
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
} else if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Try detecting
|
||||
try {
|
||||
const detectedAlgorithm = performDetectPrivateKeyAlgorithm(key)
|
||||
|
||||
if (detectedAlgorithm === 'ENCRYPTED') {
|
||||
return cacheSet(privateKeysCache, key, providedAlgorithm)
|
||||
}
|
||||
return cacheSet(privateKeysCache, key, detectedAlgorithm)
|
||||
} catch (e) {
|
||||
throw cacheSet(
|
||||
privateKeysCache,
|
||||
key,
|
||||
null,
|
||||
TokenError.wrap(e, TokenError.codes.invalidKey, 'Unsupported PEM private key.')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function detectPublicKeyAlgorithms(key) {
|
||||
if (!key) {
|
||||
return 'none'
|
||||
}
|
||||
// Check cache first
|
||||
const [cached, error] = publicKeysCache.get(key) || []
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
} else if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Try detecting
|
||||
try {
|
||||
if (key instanceof Buffer) {
|
||||
key = key.toString('utf-8')
|
||||
} else if (typeof key !== 'string') {
|
||||
throw new TokenError(TokenError.codes.invalidKey, 'The public key must be a string or a buffer.')
|
||||
}
|
||||
return cacheSet(publicKeysCache, key, performDetectPublicKeyAlgorithms(key))
|
||||
} catch (e) {
|
||||
throw cacheSet(
|
||||
publicKeysCache,
|
||||
key,
|
||||
null,
|
||||
TokenError.wrap(e, TokenError.codes.invalidKey, 'Unsupported PEM public key.')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function createSignature(algorithm, key, input) {
|
||||
try {
|
||||
const type = algorithm.slice(0, 2)
|
||||
const alg = `sha${algorithm.slice(2)}`
|
||||
|
||||
let raw
|
||||
let options
|
||||
|
||||
switch (type) {
|
||||
case 'HS':
|
||||
raw = createHmac(alg, key).update(input).digest('base64')
|
||||
break
|
||||
case 'ES':
|
||||
raw = derToJose(directSign(alg, Buffer.from(input, 'utf-8'), key), algorithm).toString('base64')
|
||||
break
|
||||
case 'RS':
|
||||
case 'PS':
|
||||
options = {
|
||||
key,
|
||||
padding: RSA_PKCS1_PADDING,
|
||||
saltLength: RSA_PSS_SALTLEN_MAX_SIGN
|
||||
}
|
||||
|
||||
if (type === 'PS') {
|
||||
options.padding = RSA_PKCS1_PSS_PADDING
|
||||
options.saltLength = RSA_PSS_SALTLEN_DIGEST
|
||||
}
|
||||
|
||||
raw = createSign(alg).update(input).sign(options).toString('base64')
|
||||
break
|
||||
case 'Ed':
|
||||
raw = directSign(undefined, Buffer.from(input, 'utf-8'), key).toString('base64')
|
||||
}
|
||||
|
||||
return raw.replace(base64UrlMatcher, base64UrlReplacer)
|
||||
} catch (e) {
|
||||
/* istanbul ignore next */
|
||||
throw new TokenError(TokenError.codes.signError, 'Cannot create the signature.', { originalError: e })
|
||||
}
|
||||
}
|
||||
|
||||
function verifySignature(algorithm, key, input, signature) {
|
||||
try {
|
||||
const type = algorithm.slice(0, 2)
|
||||
const alg = `SHA${algorithm.slice(2)}`
|
||||
|
||||
signature = Buffer.from(signature, 'base64')
|
||||
|
||||
if (type === 'HS') {
|
||||
try {
|
||||
return timingSafeEqual(createHmac(alg, key).update(input).digest(), signature)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
} else if (type === 'Ed') {
|
||||
// Check if supported on Node 10
|
||||
/* istanbul ignore next */
|
||||
if (typeof directVerify === 'function') {
|
||||
return directVerify(undefined, Buffer.from(input, 'utf-8'), key, signature)
|
||||
} else {
|
||||
throw new TokenError(TokenError.codes.signError, 'EdDSA algorithms are not supported by your Node.js version.')
|
||||
}
|
||||
}
|
||||
|
||||
const options = { key, padding: RSA_PKCS1_PADDING, saltLength: RSA_PSS_SALTLEN_AUTO }
|
||||
|
||||
if (type === 'PS') {
|
||||
options.padding = RSA_PKCS1_PSS_PADDING
|
||||
options.saltLength = RSA_PSS_SALTLEN_DIGEST
|
||||
} else if (type === 'ES') {
|
||||
signature = joseToDer(signature, algorithm)
|
||||
}
|
||||
|
||||
return createVerify('RSA-' + alg)
|
||||
.update(input)
|
||||
.verify(options, signature)
|
||||
} catch (e) {
|
||||
/* istanbul ignore next */
|
||||
throw new TokenError(TokenError.codes.verifyError, 'Cannot verify the signature.', { originalError: e })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
base64UrlMatcher,
|
||||
base64UrlReplacer,
|
||||
hsAlgorithms,
|
||||
rsaAlgorithms,
|
||||
esAlgorithms,
|
||||
edAlgorithms,
|
||||
detectPrivateKeyAlgorithm,
|
||||
detectPublicKeyAlgorithms,
|
||||
createSignature,
|
||||
verifySignature
|
||||
}
|
||||
60
backend/node_modules/fast-jwt/src/decoder.js
generated
vendored
Normal file
60
backend/node_modules/fast-jwt/src/decoder.js
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
'use strict'
|
||||
|
||||
const { TokenError } = require('./error')
|
||||
|
||||
function decode({ complete, checkTyp }, token) {
|
||||
// Make sure the token is a string or a Buffer - Other cases make no sense to even try to validate
|
||||
if (token instanceof Buffer) {
|
||||
token = token.toString('utf-8')
|
||||
} else if (typeof token !== 'string') {
|
||||
throw new TokenError(TokenError.codes.invalidType, 'The token must be a string or a buffer.')
|
||||
}
|
||||
|
||||
// Validate the format
|
||||
const firstSeparator = token.indexOf('.')
|
||||
const lastSeparator = token.lastIndexOf('.')
|
||||
|
||||
if (firstSeparator === -1 || firstSeparator >= lastSeparator) {
|
||||
throw new TokenError(TokenError.codes.malformed, 'The token is malformed.')
|
||||
}
|
||||
|
||||
// Parse header
|
||||
let validHeader = false
|
||||
try {
|
||||
const header = JSON.parse(Buffer.from(token.slice(0, firstSeparator), 'base64').toString('utf-8'))
|
||||
if (checkTyp && header.typ !== checkTyp) {
|
||||
throw new TokenError(TokenError.codes.invalidType, `The type must be "${checkTyp}".`, { header })
|
||||
}
|
||||
validHeader = true
|
||||
|
||||
// Parse payload
|
||||
let payload = Buffer.from(token.slice(firstSeparator + 1, lastSeparator), 'base64').toString('utf-8')
|
||||
payload = JSON.parse(payload)
|
||||
// https://tools.ietf.org/html/rfc7519#section-7.2
|
||||
//
|
||||
// 10. Verify that the resulting octet sequence is a UTF-8-encoded
|
||||
// representation of a completely valid JSON object conforming to
|
||||
// RFC 7159 [RFC7159]; let the JWT Claims Set be this JSON object.
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new TokenError(TokenError.codes.invalidPayload, 'The payload must be an object', { payload })
|
||||
}
|
||||
|
||||
// Return whatever was requested
|
||||
return complete
|
||||
? { header, payload, signature: token.slice(lastSeparator + 1), input: token.slice(0, lastSeparator) }
|
||||
: payload
|
||||
} catch (e) {
|
||||
throw TokenError.wrap(
|
||||
e,
|
||||
TokenError.codes.malformed,
|
||||
`The token ${validHeader ? 'payload' : 'header'} is not a valid base64url serialized JSON.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function createDecoder(options = {}) {
|
||||
const complete = options.complete || false
|
||||
const checkTyp = options.checkTyp
|
||||
|
||||
return decode.bind(null, { complete, checkTyp })
|
||||
}
|
||||
51
backend/node_modules/fast-jwt/src/error.js
generated
vendored
Normal file
51
backend/node_modules/fast-jwt/src/error.js
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict'
|
||||
|
||||
const TOKEN_ERROR_CODES = {
|
||||
invalidType: 'FAST_JWT_INVALID_TYPE', // Invalid token type
|
||||
invalidOption: 'FAST_JWT_INVALID_OPTION', // The option object is not valid
|
||||
invalidAlgorithm: 'FAST_JWT_INVALID_ALGORITHM', // The token algorithm is invalid
|
||||
invalidClaimType: 'FAST_JWT_INVALID_CLAIM_TYPE', // The claim type is not supported
|
||||
invalidClaimValue: 'FAST_JWT_INVALID_CLAIM_VALUE', // The claim type is not a positive integer or an number array
|
||||
invalidKey: 'FAST_JWT_INVALID_KEY', // The key is not a string or a buffer or is unsupported
|
||||
invalidSignature: 'FAST_JWT_INVALID_SIGNATURE', // The token signature is invalid
|
||||
invalidPayload: 'FAST_JWT_INVALID_PAYLOAD', // The payload to be decoded must be an object
|
||||
malformed: 'FAST_JWT_MALFORMED', // The token is malformed
|
||||
inactive: 'FAST_JWT_INACTIVE', // The token is not valid yet
|
||||
expired: 'FAST_JWT_EXPIRED', // The token is expired
|
||||
missingKey: 'FAST_JWT_MISSING_KEY', // The key option is missing
|
||||
keyFetchingError: 'FAST_JWT_KEY_FETCHING_ERROR', // Could not retrieve the key
|
||||
signError: 'FAST_JWT_SIGN_ERROR', // Cannot create the signature
|
||||
verifyError: 'FAST_JWT_VERIFY_ERROR', // Cannot verify the signature
|
||||
missingRequiredClaim: 'FAST_JWT_MISSING_REQUIRED_CLAIM', // A required claim is missing
|
||||
missingSignature: 'FAST_JWT_MISSING_SIGNATURE' // The token signature is missing
|
||||
}
|
||||
|
||||
class TokenError extends Error {
|
||||
constructor(code, message, additional) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
|
||||
this.code = code
|
||||
|
||||
if (additional) {
|
||||
for (const k in additional) {
|
||||
this[k] = additional[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TokenError.codes = TOKEN_ERROR_CODES
|
||||
|
||||
TokenError.wrap = function (originalError, code, message) {
|
||||
if (originalError instanceof TokenError) {
|
||||
return originalError
|
||||
}
|
||||
|
||||
return new TokenError(code, message, { originalError })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TokenError,
|
||||
TOKEN_ERROR_CODES
|
||||
}
|
||||
157
backend/node_modules/fast-jwt/src/index.d.ts
generated
vendored
Normal file
157
backend/node_modules/fast-jwt/src/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
export type Algorithm =
|
||||
| 'none'
|
||||
| 'HS256'
|
||||
| 'HS384'
|
||||
| 'HS512'
|
||||
| 'ES256'
|
||||
| 'ES384'
|
||||
| 'ES512'
|
||||
| 'RS256'
|
||||
| 'RS384'
|
||||
| 'RS512'
|
||||
| 'PS256'
|
||||
| 'PS384'
|
||||
| 'PS512'
|
||||
| 'EdDSA'
|
||||
|
||||
export type TokenValidationErrorCode =
|
||||
| 'FAST_JWT_INVALID_TYPE'
|
||||
| 'FAST_JWT_INVALID_OPTION'
|
||||
| 'FAST_JWT_INVALID_ALGORITHM'
|
||||
| 'FAST_JWT_INVALID_CLAIM_TYPE'
|
||||
| 'FAST_JWT_INVALID_CLAIM_VALUE'
|
||||
| 'FAST_JWT_INVALID_KEY'
|
||||
| 'FAST_JWT_INVALID_SIGNATURE'
|
||||
| 'FAST_JWT_INVALID_PAYLOAD'
|
||||
| 'FAST_JWT_MALFORMED'
|
||||
| 'FAST_JWT_INACTIVE'
|
||||
| 'FAST_JWT_EXPIRED'
|
||||
| 'FAST_JWT_MISSING_KEY'
|
||||
| 'FAST_JWT_KEY_FETCHING_ERROR'
|
||||
| 'FAST_JWT_SIGN_ERROR'
|
||||
| 'FAST_JWT_VERIFY_ERROR'
|
||||
| 'FAST_JWT_MISSING_REQUIRED_CLAIM'
|
||||
| 'FAST_JWT_MISSING_SIGNATURE'
|
||||
|
||||
declare class TokenError extends Error {
|
||||
static wrap(originalError: Error, code: TokenValidationErrorCode, message: string): TokenError
|
||||
static codes: {
|
||||
invalidType: 'FAST_JWT_INVALID_TYPE'
|
||||
invalidOption: 'FAST_JWT_INVALID_OPTION'
|
||||
invalidAlgorithm: 'FAST_JWT_INVALID_ALGORITHM'
|
||||
invalidClaimType: 'FAST_JWT_INVALID_CLAIM_TYPE'
|
||||
invalidClaimValue: 'FAST_JWT_INVALID_CLAIM_VALUE'
|
||||
invalidKey: 'FAST_JWT_INVALID_KEY'
|
||||
invalidSignature: 'FAST_JWT_INVALID_SIGNATURE'
|
||||
invalidPayload: 'FAST_JWT_INVALID_PAYLOAD'
|
||||
malformed: 'FAST_JWT_MALFORMED'
|
||||
inactive: 'FAST_JWT_INACTIVE'
|
||||
expired: 'FAST_JWT_EXPIRED'
|
||||
missingKey: 'FAST_JWT_MISSING_KEY'
|
||||
keyFetchingError: 'FAST_JWT_KEY_FETCHING_ERROR'
|
||||
signError: 'FAST_JWT_SIGN_ERROR'
|
||||
verifyError: 'FAST_JWT_VERIFY_ERROR'
|
||||
missingRequiredClaim: 'FAST_JWT_MISSING_REQUIRED_CLAIM'
|
||||
missingSignature: 'FAST_JWT_MISSING_SIGNATURE'
|
||||
}
|
||||
|
||||
code: TokenValidationErrorCode;
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type SignerCallback = (e: Error | TokenError | null, token: string) => void
|
||||
type VerifierCallback = (e: Error | TokenError | null, payload: any) => void
|
||||
|
||||
type DecodedJwt = {
|
||||
header: Record<string, any>
|
||||
payload: any
|
||||
signature: string
|
||||
input: string
|
||||
}
|
||||
|
||||
type Bufferable = string | Buffer
|
||||
|
||||
type KeyFetcher =
|
||||
| ((DecodedJwt: DecodedJwt) => Promise<Bufferable>)
|
||||
| ((DecodedJwt: DecodedJwt, cb: (err: Error | TokenError | null, key: Bufferable) => void) => void)
|
||||
|
||||
type SignerPayload = Bufferable | Record<string, any>
|
||||
|
||||
declare function SignerSync<T = SignerPayload>(payload: T): string
|
||||
declare function SignerAsync<T = SignerPayload>(payload: T): Promise<string>
|
||||
declare function SignerAsync<T = SignerPayload>(payload: T, cb: SignerCallback): void
|
||||
|
||||
declare function VerifierSync<T = Bufferable>(token: T): any
|
||||
declare function VerifierAsync<T = Bufferable>(token: T): Promise<any>
|
||||
declare function VerifierAsync<T = Bufferable>(token: T, cb: VerifierCallback): void
|
||||
|
||||
export interface JwtHeader extends Record<string, any> {
|
||||
alg: string | Algorithm
|
||||
typ?: string
|
||||
cty?: string
|
||||
crit?: Array<string | Exclude<keyof JwtHeader, 'crit'>>
|
||||
kid?: string
|
||||
jku?: string
|
||||
x5u?: string | string[]
|
||||
'x5t#S256'?: string
|
||||
x5t?: string
|
||||
x5c?: string | string[]
|
||||
}
|
||||
|
||||
export interface SignerOptions {
|
||||
algorithm?: Algorithm
|
||||
mutatePayload?: boolean
|
||||
expiresIn?: number | string
|
||||
notBefore?: number
|
||||
jti?: string
|
||||
aud?: string | string[]
|
||||
iss?: string
|
||||
sub?: string
|
||||
nonce?: string
|
||||
kid?: string
|
||||
header?: JwtHeader
|
||||
noTimestamp?: boolean
|
||||
clockTimestamp?: number
|
||||
}
|
||||
|
||||
export interface DecoderOptions {
|
||||
complete?: boolean
|
||||
checkTyp?: string
|
||||
}
|
||||
|
||||
type VerifierAllowedBase = string | RegExp
|
||||
type VerifierAllowed = VerifierAllowedBase | Array<VerifierAllowedBase>
|
||||
|
||||
export interface VerifierOptions {
|
||||
algorithms?: Algorithm[]
|
||||
complete?: boolean
|
||||
cache?: boolean | number
|
||||
cacheTTL?: number
|
||||
errorCacheTTL?: number | ((tokenError: TokenError) => number)
|
||||
allowedJti?: VerifierAllowed
|
||||
allowedAud?: VerifierAllowed
|
||||
allowedIss?: VerifierAllowed
|
||||
allowedSub?: VerifierAllowed
|
||||
allowedNonce?: VerifierAllowed
|
||||
ignoreExpiration?: boolean
|
||||
ignoreNotBefore?: boolean
|
||||
maxAge?: number
|
||||
clockTimestamp?: number
|
||||
clockTolerance?: number
|
||||
requiredClaims?: Array<string>
|
||||
checkTyp?: string
|
||||
cacheKeyBuilder?: (token: string) => string
|
||||
}
|
||||
|
||||
export interface PrivateKey {
|
||||
key: Bufferable
|
||||
passphrase: string | undefined
|
||||
}
|
||||
|
||||
export function createSigner<T = SignerPayload>(
|
||||
options?: Partial<SignerOptions & { key: Bufferable | PrivateKey }>
|
||||
): typeof SignerSync<T>
|
||||
export function createSigner<T = SignerPayload>(options?: Partial<SignerOptions & { key: KeyFetcher }>): typeof SignerAsync<T>
|
||||
export function createDecoder(options?: Partial<DecoderOptions>): (token: Bufferable) => any
|
||||
export function createVerifier<T = Bufferable>(options?: Partial<VerifierOptions & { key: Bufferable }>): typeof VerifierSync<T>
|
||||
export function createVerifier<T = Bufferable>(options?: Partial<VerifierOptions & { key: KeyFetcher }>): typeof VerifierAsync<T>
|
||||
14
backend/node_modules/fast-jwt/src/index.js
generated
vendored
Normal file
14
backend/node_modules/fast-jwt/src/index.js
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
const { TokenError, TOKEN_ERROR_CODES } = require('./error')
|
||||
const createDecoder = require('./decoder')
|
||||
const createVerifier = require('./verifier')
|
||||
const createSigner = require('./signer')
|
||||
|
||||
module.exports = {
|
||||
TokenError,
|
||||
TOKEN_ERROR_CODES,
|
||||
createDecoder,
|
||||
createVerifier,
|
||||
createSigner
|
||||
}
|
||||
313
backend/node_modules/fast-jwt/src/signer.js
generated
vendored
Normal file
313
backend/node_modules/fast-jwt/src/signer.js
generated
vendored
Normal file
@@ -0,0 +1,313 @@
|
||||
'use strict'
|
||||
|
||||
const {
|
||||
base64UrlMatcher,
|
||||
base64UrlReplacer,
|
||||
hsAlgorithms,
|
||||
esAlgorithms,
|
||||
rsaAlgorithms,
|
||||
edAlgorithms,
|
||||
detectPrivateKeyAlgorithm,
|
||||
createSignature
|
||||
} = require('./crypto')
|
||||
const { TokenError } = require('./error')
|
||||
const { getAsyncKey, ensurePromiseCallback } = require('./utils')
|
||||
const { createPrivateKey, createSecretKey } = require('node:crypto')
|
||||
const { parse: parseMs } = require('@lukeed/ms')
|
||||
|
||||
const supportedAlgorithms = new Set([...hsAlgorithms, ...esAlgorithms, ...rsaAlgorithms, ...edAlgorithms, 'none'])
|
||||
|
||||
const supportedAlgorithmsList = Array.from(supportedAlgorithms).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')
|
||||
}
|
||||
|
||||
return algorithm[0] === 'H' ? createSecretKey(key) : createPrivateKey(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: payload.nbf ? payload.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, payload }, (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 && !supportedAlgorithms.has(algorithm)) {
|
||||
throw new TokenError(
|
||||
TokenError.codes.invalidOption,
|
||||
`The algorithm option must be one of the following values: ${supportedAlgorithmsList}.`
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
if (typeof expiresIn === 'string') {
|
||||
expiresIn = parseMs(expiresIn)
|
||||
}
|
||||
if (typeof expiresIn !== 'number' || expiresIn < 0) {
|
||||
throw new TokenError(
|
||||
TokenError.codes.invalidOption,
|
||||
'The expiresIn option must be a positive number or a valid string.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (notBefore) {
|
||||
if (typeof notBefore === 'string') {
|
||||
notBefore = parseMs(notBefore)
|
||||
}
|
||||
if (typeof notBefore !== 'number' || notBefore < 0) {
|
||||
throw new TokenError(
|
||||
TokenError.codes.invalidOption,
|
||||
'The notBefore option must be a positive number or a valid string.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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.entries(fpo).reduce((obj, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
obj[key] = value
|
||||
}
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
// Return the signer
|
||||
return sign.bind(null, {
|
||||
key,
|
||||
algorithm,
|
||||
noTimestamp,
|
||||
mutatePayload,
|
||||
clockTimestamp,
|
||||
expiresIn,
|
||||
notBefore,
|
||||
kid,
|
||||
typ,
|
||||
isAsync: keyType === 'function',
|
||||
additionalHeader,
|
||||
fixedPayload
|
||||
})
|
||||
}
|
||||
65
backend/node_modules/fast-jwt/src/utils.js
generated
vendored
Normal file
65
backend/node_modules/fast-jwt/src/utils.js
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
'use strict'
|
||||
|
||||
const { createHash } = require('node:crypto')
|
||||
const algorithmMatcher = /"alg"\s*:\s*"[HERP]S(256|384)"/m
|
||||
const edAlgorithmMatcher = /"alg"\s*:\s*"EdDSA"/m
|
||||
const ed448CurveMatcher = /"crv"\s*:\s*"Ed448"/m
|
||||
|
||||
function getAsyncKey(handler, decoded, callback) {
|
||||
const result = handler(decoded, callback)
|
||||
|
||||
if (result && typeof result.then === 'function') {
|
||||
result
|
||||
.then(key => {
|
||||
// This avoids the callback to be thrown twice if callback throws
|
||||
process.nextTick(() => callback(null, key))
|
||||
})
|
||||
.catch(callback)
|
||||
}
|
||||
}
|
||||
|
||||
function ensurePromiseCallback(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
return [callback]
|
||||
}
|
||||
|
||||
let promiseResolve, promiseReject
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
promiseResolve = resolve
|
||||
promiseReject = reject
|
||||
})
|
||||
|
||||
return [
|
||||
function (err, token) {
|
||||
if (err) {
|
||||
return promiseReject(err)
|
||||
}
|
||||
|
||||
return promiseResolve(token)
|
||||
},
|
||||
promise
|
||||
]
|
||||
}
|
||||
|
||||
function hashToken(token) {
|
||||
const rawHeader = token.split('.', 1)[0]
|
||||
const header = Buffer.from(rawHeader, 'base64').toString('utf-8')
|
||||
let hasher = null
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (header.match(edAlgorithmMatcher) && header.match(ed448CurveMatcher)) {
|
||||
hasher = createHash('shake256', { outputLength: 114 })
|
||||
} else {
|
||||
const mo = header.match(algorithmMatcher)
|
||||
hasher = createHash(`sha${mo ? mo[1] : '512'}`)
|
||||
}
|
||||
|
||||
return hasher.update(token).digest('hex')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAsyncKey,
|
||||
ensurePromiseCallback,
|
||||
hashToken
|
||||
}
|
||||
524
backend/node_modules/fast-jwt/src/verifier.js
generated
vendored
Normal file
524
backend/node_modules/fast-jwt/src/verifier.js
generated
vendored
Normal file
@@ -0,0 +1,524 @@
|
||||
'use strict'
|
||||
|
||||
const { createPublicKey, createSecretKey } = require('node:crypto')
|
||||
const Cache = require('mnemonist/lru-cache')
|
||||
|
||||
const { hsAlgorithms, verifySignature, detectPublicKeyAlgorithms } = require('./crypto')
|
||||
const createDecoder = require('./decoder')
|
||||
const { TokenError } = require('./error')
|
||||
const { getAsyncKey, ensurePromiseCallback, hashToken } = require('./utils')
|
||||
|
||||
const defaultCacheSize = 1000
|
||||
|
||||
function exactStringClaimMatcher(allowed, actual) {
|
||||
return allowed === actual
|
||||
}
|
||||
|
||||
function checkAreCompatibleAlgorithms(expected, actual) {
|
||||
for (const expectedAlg of expected) {
|
||||
// if at least one of the expected algorithms is compatible we're done
|
||||
if (actual.includes(expectedAlg)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
throw new TokenError(
|
||||
TokenError.codes.invalidKey,
|
||||
`Invalid public key provided for algorithms ${expected.join(', ')}.`
|
||||
)
|
||||
}
|
||||
|
||||
function prepareKeyOrSecret(key, isSecret) {
|
||||
if (typeof key === 'string') {
|
||||
key = Buffer.from(key, 'utf-8')
|
||||
}
|
||||
|
||||
return isSecret ? createSecretKey(key) : createPublicKey(key)
|
||||
}
|
||||
|
||||
function ensureStringClaimMatcher(raw) {
|
||||
if (!Array.isArray(raw)) {
|
||||
raw = [raw]
|
||||
}
|
||||
|
||||
return raw
|
||||
.filter(r => r)
|
||||
.map(r => {
|
||||
if (r && typeof r.test === 'function') {
|
||||
return r
|
||||
}
|
||||
|
||||
return { test: exactStringClaimMatcher.bind(null, r) }
|
||||
})
|
||||
}
|
||||
|
||||
function createCache(rawSize) {
|
||||
const size = parseInt(rawSize === true ? defaultCacheSize : rawSize, 10)
|
||||
return size > 0 ? new Cache(size) : null
|
||||
}
|
||||
|
||||
function cacheSet(
|
||||
{
|
||||
cache,
|
||||
token,
|
||||
cacheTTL,
|
||||
payload,
|
||||
ignoreExpiration,
|
||||
ignoreNotBefore,
|
||||
maxAge,
|
||||
clockTimestamp = Date.now(),
|
||||
clockTolerance,
|
||||
errorCacheTTL,
|
||||
cacheKeyBuilder
|
||||
},
|
||||
value
|
||||
) {
|
||||
if (!cache) {
|
||||
return value
|
||||
}
|
||||
|
||||
const cacheValue = [value, 0, 0]
|
||||
|
||||
if (value instanceof TokenError) {
|
||||
const ttl = typeof errorCacheTTL === 'function' ? errorCacheTTL(value) : errorCacheTTL
|
||||
cacheValue[2] = clockTimestamp + clockTolerance + ttl
|
||||
cache.set(cacheKeyBuilder(token), cacheValue)
|
||||
return value
|
||||
}
|
||||
|
||||
const hasIat = payload && typeof payload.iat === 'number'
|
||||
|
||||
// Add time range of the token
|
||||
if (hasIat) {
|
||||
cacheValue[1] = !ignoreNotBefore && typeof payload.nbf === 'number' ? payload.nbf * 1000 - clockTolerance : 0
|
||||
|
||||
if (!ignoreExpiration) {
|
||||
if (typeof payload.exp === 'number') {
|
||||
cacheValue[2] = payload.exp * 1000 + clockTolerance
|
||||
} else if (maxAge) {
|
||||
cacheValue[2] = payload.iat * 1000 + maxAge + clockTolerance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The maximum TTL for the token cannot exceed the configured cacheTTL
|
||||
const maxTTL = clockTimestamp + clockTolerance + cacheTTL
|
||||
cacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL)
|
||||
|
||||
cache.set(cacheKeyBuilder(token), cacheValue)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function handleCachedResult(cached, callback, promise) {
|
||||
if (cached instanceof TokenError) {
|
||||
if (!callback) {
|
||||
throw cached
|
||||
}
|
||||
|
||||
callback(cached)
|
||||
} else {
|
||||
if (!callback) {
|
||||
return cached
|
||||
}
|
||||
|
||||
callback(null, cached)
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
function validateAlgorithmAndSignature(input, header, signature, key, allowedAlgorithms) {
|
||||
// According to the signature and key, check with algorithms are supported
|
||||
// Verify the token is allowed
|
||||
if (!allowedAlgorithms.includes(header.alg)) {
|
||||
throw new TokenError(TokenError.codes.invalidAlgorithm, 'The token algorithm is invalid.')
|
||||
}
|
||||
|
||||
// Verify the signature, if present
|
||||
if (signature && !verifySignature(header.alg, key, input, signature)) {
|
||||
throw new TokenError(TokenError.codes.invalidSignature, 'The token signature is invalid.')
|
||||
}
|
||||
}
|
||||
|
||||
function validateClaimType(values, claim, array, type) {
|
||||
const typeFailureMessage = array
|
||||
? `The ${claim} claim must be a ${type} or an array of ${type}s.`
|
||||
: `The ${claim} claim must be a ${type}.`
|
||||
|
||||
if (values.map(v => typeof v).some(t => t !== type)) {
|
||||
throw new TokenError(TokenError.codes.invalidClaimType, typeFailureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
function validateClaimValues(values, claim, allowed, arrayValue) {
|
||||
const failureMessage = arrayValue
|
||||
? `Not all of the ${claim} claim values are allowed.`
|
||||
: `The ${claim} claim value is not allowed.`
|
||||
|
||||
if (!values.every(v => allowed.some(a => a.test(v)))) {
|
||||
throw new TokenError(TokenError.codes.invalidClaimValue, failureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
function validateClaimArrayValues(values, claim, allowed, arrayValue) {
|
||||
const failureMessage = arrayValue
|
||||
? `None of ${claim} claim values are allowed.`
|
||||
: `The ${claim} claim value is not allowed.`
|
||||
|
||||
if (!values.some(v => allowed.some(a => a.test(v)))) {
|
||||
throw new TokenError(TokenError.codes.invalidClaimValue, failureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
function validateClaimDateValue(value, modifier, now, greater, errorCode, errorVerb) {
|
||||
const adjusted = value * 1000 + (modifier || 0)
|
||||
const valid = greater ? now >= adjusted : now <= adjusted
|
||||
|
||||
if (!valid) {
|
||||
throw new TokenError(TokenError.codes[errorCode], `The token ${errorVerb} at ${new Date(adjusted).toISOString()}.`)
|
||||
}
|
||||
}
|
||||
|
||||
function verifyToken(
|
||||
key,
|
||||
{ input, header, payload, signature },
|
||||
{ validators, allowedAlgorithms, checkTyp, clockTimestamp, requiredClaims }
|
||||
) {
|
||||
// Verify the key
|
||||
/* istanbul ignore next */
|
||||
const hasKey = key instanceof Buffer ? key.length : !!key
|
||||
|
||||
if (hasKey && !signature) {
|
||||
throw new TokenError(TokenError.codes.missingSignature, 'The token signature is missing.')
|
||||
} else if (!hasKey && signature) {
|
||||
throw new TokenError(TokenError.codes.missingKey, 'The key option is missing.')
|
||||
}
|
||||
|
||||
validateAlgorithmAndSignature(input, header, signature, key, allowedAlgorithms)
|
||||
|
||||
// Verify typ
|
||||
if (
|
||||
checkTyp &&
|
||||
(typeof header.typ !== 'string' || checkTyp !== header.typ.toLowerCase().replace(/^application\//, ''))
|
||||
) {
|
||||
throw new TokenError(TokenError.codes.invalidType, 'Invalid typ.')
|
||||
}
|
||||
|
||||
if (requiredClaims) {
|
||||
for (const claim of requiredClaims) {
|
||||
if (!(claim in payload)) {
|
||||
throw new TokenError(TokenError.codes.missingRequiredClaim, `The ${claim} claim is required.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the payload
|
||||
const now = clockTimestamp || Date.now()
|
||||
|
||||
for (const { type, claim, allowed, array, modifier, greater, errorCode, errorVerb } of validators) {
|
||||
const value = payload[claim]
|
||||
const arrayValue = Array.isArray(value)
|
||||
const values = arrayValue ? value : [value]
|
||||
|
||||
// We have already checked above that all required claims are present
|
||||
// Therefore we can skip this validator if the claim is not present
|
||||
if (!(claim in payload)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate type
|
||||
validateClaimType(values, claim, array, type === 'date' ? 'number' : 'string')
|
||||
|
||||
if (type === 'date') {
|
||||
validateClaimDateValue(value, modifier, now, greater, errorCode, errorVerb)
|
||||
} else if (array) {
|
||||
validateClaimArrayValues(values, claim, allowed, arrayValue)
|
||||
} else {
|
||||
validateClaimValues(values, claim, allowed, arrayValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function verify(
|
||||
{
|
||||
key,
|
||||
allowedAlgorithms,
|
||||
complete,
|
||||
cacheTTL,
|
||||
checkTyp,
|
||||
clockTimestamp,
|
||||
clockTolerance,
|
||||
ignoreExpiration,
|
||||
ignoreNotBefore,
|
||||
maxAge,
|
||||
isAsync,
|
||||
validators,
|
||||
decode,
|
||||
cache,
|
||||
requiredClaims,
|
||||
errorCacheTTL,
|
||||
cacheKeyBuilder
|
||||
},
|
||||
token,
|
||||
cb
|
||||
) {
|
||||
const [callback, promise] = isAsync ? ensurePromiseCallback(cb) : []
|
||||
|
||||
// Check the cache
|
||||
if (cache) {
|
||||
const [value, min, max] = cache.get(cacheKeyBuilder(token)) || [undefined, 0, 0]
|
||||
const now = clockTimestamp || Date.now()
|
||||
|
||||
// Validate time range
|
||||
if (
|
||||
/* istanbul ignore next */
|
||||
typeof value !== 'undefined' &&
|
||||
(min === 0 ||
|
||||
(now < min && value.code === 'FAST_JWT_INACTIVE') ||
|
||||
(now >= min && value.code !== 'FAST_JWT_INACTIVE')) &&
|
||||
(max === 0 || now <= max)
|
||||
) {
|
||||
// Cache hit
|
||||
return handleCachedResult(value, callback, promise)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
As very first thing, decode the token - If invalid, everything else is useless.
|
||||
We don't involve cache here since it's much slower.
|
||||
*/
|
||||
let decoded
|
||||
try {
|
||||
decoded = decode(token)
|
||||
} catch (e) {
|
||||
if (callback) {
|
||||
callback(e)
|
||||
return promise
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
|
||||
const { header, payload, signature, input } = decoded
|
||||
const cacheContext = {
|
||||
cache,
|
||||
token,
|
||||
cacheTTL,
|
||||
errorCacheTTL,
|
||||
ignoreExpiration,
|
||||
ignoreNotBefore,
|
||||
maxAge,
|
||||
clockTimestamp,
|
||||
clockTolerance,
|
||||
payload,
|
||||
cacheKeyBuilder
|
||||
}
|
||||
const validationContext = { validators, allowedAlgorithms, checkTyp, clockTimestamp, clockTolerance, requiredClaims }
|
||||
|
||||
// We have the key
|
||||
if (!callback) {
|
||||
try {
|
||||
verifyToken(key, decoded, validationContext)
|
||||
|
||||
return cacheSet(cacheContext, complete ? { header, payload, signature, input } : payload)
|
||||
} catch (e) {
|
||||
throw cacheSet(cacheContext, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the key asynchronously
|
||||
getAsyncKey(key, { header, payload, signature }, (err, currentKey) => {
|
||||
if (err) {
|
||||
return callback(
|
||||
cacheSet(cacheContext, TokenError.wrap(err, TokenError.codes.keyFetchingError, 'Cannot fetch key.'))
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof currentKey === 'string') {
|
||||
currentKey = Buffer.from(currentKey, 'utf-8')
|
||||
} else if (!(currentKey instanceof Buffer)) {
|
||||
return callback(
|
||||
cacheSet(
|
||||
cacheContext,
|
||||
new TokenError(
|
||||
TokenError.codes.keyFetchingError,
|
||||
'The key returned from the callback must be a string or a buffer containing a secret or a public key.'
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
// Detect the private key - If the algorithms were known, just verify they match, otherwise assign them
|
||||
const availableAlgorithms = detectPublicKeyAlgorithms(currentKey)
|
||||
|
||||
if (validationContext.allowedAlgorithms.length) {
|
||||
checkAreCompatibleAlgorithms(allowedAlgorithms, availableAlgorithms)
|
||||
} else {
|
||||
validationContext.allowedAlgorithms = availableAlgorithms
|
||||
}
|
||||
|
||||
currentKey = prepareKeyOrSecret(currentKey, availableAlgorithms[0] === hsAlgorithms[0])
|
||||
|
||||
verifyToken(currentKey, decoded, validationContext)
|
||||
} catch (e) {
|
||||
return callback(cacheSet(cacheContext, e))
|
||||
}
|
||||
|
||||
callback(null, cacheSet(cacheContext, complete ? { header, payload, signature, input: token } : payload))
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
module.exports = function createVerifier(options) {
|
||||
let {
|
||||
key,
|
||||
algorithms: allowedAlgorithms,
|
||||
complete,
|
||||
cache: cacheSize,
|
||||
cacheTTL,
|
||||
errorCacheTTL,
|
||||
checkTyp,
|
||||
clockTimestamp,
|
||||
clockTolerance,
|
||||
ignoreExpiration,
|
||||
ignoreNotBefore,
|
||||
maxAge,
|
||||
allowedJti,
|
||||
allowedAud,
|
||||
allowedIss,
|
||||
allowedSub,
|
||||
allowedNonce,
|
||||
requiredClaims,
|
||||
cacheKeyBuilder
|
||||
} = { cacheTTL: 600_000, clockTolerance: 0, errorCacheTTL: -1, cacheKeyBuilder: hashToken, ...options }
|
||||
|
||||
// Validate options
|
||||
if (!Array.isArray(allowedAlgorithms)) {
|
||||
allowedAlgorithms = []
|
||||
}
|
||||
|
||||
const keyType = typeof key
|
||||
if (keyType !== 'string' && keyType !== 'object' && keyType !== 'function') {
|
||||
throw new TokenError(
|
||||
TokenError.codes.INVALID_OPTION,
|
||||
'The key option must be a string, a buffer or a function returning the algorithm secret or public key.'
|
||||
)
|
||||
}
|
||||
|
||||
if (key && keyType !== 'function') {
|
||||
// Detect the private key - If the algorithms were known, just verify they match, otherwise assign them
|
||||
const availableAlgorithms = detectPublicKeyAlgorithms(key)
|
||||
|
||||
if (allowedAlgorithms.length) {
|
||||
checkAreCompatibleAlgorithms(allowedAlgorithms, availableAlgorithms)
|
||||
} else {
|
||||
allowedAlgorithms = availableAlgorithms
|
||||
}
|
||||
|
||||
key = prepareKeyOrSecret(key, availableAlgorithms[0] === hsAlgorithms[0])
|
||||
}
|
||||
|
||||
if (clockTimestamp && (typeof clockTimestamp !== 'number' || clockTimestamp < 0)) {
|
||||
throw new TokenError(TokenError.codes.invalidOption, 'The clockTimestamp option must be a positive number.')
|
||||
}
|
||||
|
||||
if (clockTolerance && (typeof clockTolerance !== 'number' || clockTolerance < 0)) {
|
||||
throw new TokenError(TokenError.codes.invalidOption, 'The clockTolerance option must be a positive number.')
|
||||
}
|
||||
|
||||
if (cacheTTL && (typeof cacheTTL !== 'number' || cacheTTL < 0)) {
|
||||
throw new TokenError(TokenError.codes.invalidOption, 'The cacheTTL option must be a positive number.')
|
||||
}
|
||||
|
||||
if (
|
||||
(errorCacheTTL && typeof errorCacheTTL !== 'function' && typeof errorCacheTTL !== 'number') ||
|
||||
errorCacheTTL < -1
|
||||
) {
|
||||
throw new TokenError(
|
||||
TokenError.codes.invalidOption,
|
||||
'The errorCacheTTL option must be a number greater than -1 or a function.'
|
||||
)
|
||||
}
|
||||
|
||||
if (requiredClaims && !Array.isArray(requiredClaims)) {
|
||||
throw new TokenError(TokenError.codes.invalidOption, 'The requiredClaims option must be an array.')
|
||||
}
|
||||
|
||||
// Add validators
|
||||
const validators = []
|
||||
|
||||
if (!ignoreNotBefore) {
|
||||
validators.push({
|
||||
type: 'date',
|
||||
claim: 'nbf',
|
||||
errorCode: 'inactive',
|
||||
errorVerb: 'will be active',
|
||||
greater: true,
|
||||
modifier: -clockTolerance
|
||||
})
|
||||
}
|
||||
|
||||
if (!ignoreExpiration) {
|
||||
validators.push({
|
||||
type: 'date',
|
||||
claim: 'exp',
|
||||
errorCode: 'expired',
|
||||
errorVerb: 'has expired',
|
||||
modifier: +clockTolerance
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof maxAge === 'number') {
|
||||
validators.push({ type: 'date', claim: 'iat', errorCode: 'expired', errorVerb: 'has expired', modifier: maxAge })
|
||||
}
|
||||
|
||||
if (allowedJti) {
|
||||
validators.push({ type: 'string', claim: 'jti', allowed: ensureStringClaimMatcher(allowedJti) })
|
||||
}
|
||||
|
||||
if (allowedAud) {
|
||||
validators.push({ type: 'string', claim: 'aud', allowed: ensureStringClaimMatcher(allowedAud), array: true })
|
||||
}
|
||||
|
||||
if (allowedIss) {
|
||||
validators.push({ type: 'string', claim: 'iss', allowed: ensureStringClaimMatcher(allowedIss) })
|
||||
}
|
||||
|
||||
if (allowedSub) {
|
||||
validators.push({ type: 'string', claim: 'sub', allowed: ensureStringClaimMatcher(allowedSub) })
|
||||
}
|
||||
|
||||
if (allowedNonce) {
|
||||
validators.push({ type: 'string', claim: 'nonce', allowed: ensureStringClaimMatcher(allowedNonce) })
|
||||
}
|
||||
|
||||
const normalizedTyp = checkTyp ? checkTyp.toLowerCase().replace(/^application\//, '') : null
|
||||
|
||||
const context = {
|
||||
key,
|
||||
allowedAlgorithms,
|
||||
complete,
|
||||
cacheTTL,
|
||||
errorCacheTTL,
|
||||
checkTyp: normalizedTyp,
|
||||
clockTimestamp,
|
||||
clockTolerance,
|
||||
ignoreExpiration,
|
||||
ignoreNotBefore,
|
||||
maxAge,
|
||||
isAsync: keyType === 'function',
|
||||
validators,
|
||||
decode: createDecoder({ complete: true }),
|
||||
cache: createCache(cacheSize),
|
||||
requiredClaims,
|
||||
cacheKeyBuilder
|
||||
}
|
||||
|
||||
// Return the verifier
|
||||
const verifier = verify.bind(null, context)
|
||||
verifier.cache = context.cache
|
||||
return verifier
|
||||
}
|
||||
Reference in New Issue
Block a user