UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

216 lines (215 loc) 9.01 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JWTSignatureError = exports.JWTAlgorithmError = exports.JWTClaimError = exports.JWTTimeError = exports.JWTTypeError = exports.JWTMalformedError = exports.JWTError = void 0; exports.jwtDecode = jwtDecode; exports.jwtSign = jwtSign; exports.jwtVerify = jwtVerify; const object_1 = require("@valkyriestudios/utils/object"); const number_1 = require("@valkyriestudios/utils/number"); const Crypto_1 = require("../utils/Crypto"); class JWTError extends Error { constructor(message, cause) { super(message, { cause }); this.name = 'JWTError'; } } exports.JWTError = JWTError; class JWTMalformedError extends JWTError { constructor() { super('JWT@verify: Malformed token'); this.name = 'JWTMalformedError'; } } exports.JWTMalformedError = JWTMalformedError; class JWTTypeError extends JWTError { constructor(expected, actual) { super(`JWT@verify: Invalid 'typ' header. Expected '${expected}', got '${actual}'`); this.name = 'JWTTypeError'; } } exports.JWTTypeError = JWTTypeError; class JWTTimeError extends JWTError { constructor(msg) { super(`JWT@verify: ${msg}`); this.name = 'JWTTimeError'; } } exports.JWTTimeError = JWTTimeError; class JWTClaimError extends JWTError { constructor(type) { super(`JWT@verify: INVALID_${type}`); this.name = 'JWTClaimError'; } } exports.JWTClaimError = JWTClaimError; class JWTAlgorithmError extends JWTError { constructor(reason) { super(`JWT@verify: Algorithm ${reason.toLowerCase()}`); this.name = 'JWTAlgorithmError'; } } exports.JWTAlgorithmError = JWTAlgorithmError; class JWTSignatureError extends JWTError { constructor(cause) { super('JWT@verify: INVALID_SIGNATURE', cause); this.name = 'JWTSignatureError'; } } exports.JWTSignatureError = JWTSignatureError; /** * Decodes a JWT without verifying its signature. * * @param {string} token - JWT string to decode * @returns The decoded header and payload as a combined object. * @throws JWTError if token is malformed or missing required parts. * @see https://jwt.io */ function jwtDecode(token) { if (typeof token !== 'string') throw new JWTError('JWT@decode: Invalid token'); const parts = token.split('.'); if (parts.length < 2) throw new JWTError('JWT@decode: Malformed token'); let jwt_data; try { jwt_data = JSON.parse((0, Crypto_1.utf8Decode)((0, Crypto_1.b64urlDecode)(parts[1]))); jwt_data._header = JSON.parse((0, Crypto_1.utf8Decode)((0, Crypto_1.b64urlDecode)(parts[0]))); } catch { /* Noop */ } if (!jwt_data || typeof jwt_data !== 'object' || !jwt_data?._header?.alg || typeof jwt_data?._header.alg !== 'string') throw new JWTError('JWT@decode: Missing algorithm and typ'); return jwt_data; } /** * Signs a payload into a JSON Web Token (JWT). * * @param {JWTPayload} payload - The payload to include in the token (must be a plain object) * @param {string|JsonWebKey|CryptoKey} secret - A secret, key, or CryptoKey used to sign the token * @param {JWTSignOptions} options - Signing options like `issuer`, `audience`, `expiresIn`, etc. * @returns A Promise resolving to a JWT string. * @throws Error if the payload is invalid or secret is missing. * @see https://jwt.io */ async function jwtSign(secret, options = {}) { if (!secret) throw new Error('JWT@sign: Secret must be provided'); if (!(0, object_1.isObject)(options)) throw new Error('JWT@sign: Options must be provided'); /* Current time */ const now = Math.floor(Date.now() / 1000); /* Create header */ const header = { typ: typeof options.type === 'string' ? options.type : 'JWT', alg: options.algorithm && options.algorithm in Crypto_1.ALGOS ? options.algorithm : 'HS256', }; /* Remove reserved claim keys from user payload */ /* @ts-expect-error Should be good, options.payload is raw and omit doesnt recognize the keys */ const body = (0, object_1.isObject)(options.payload) ? (0, object_1.omit)(options.payload, ['iss', 'sub', 'aud', 'exp', 'nbf', 'iat', 'jti']) : {}; // eslint-disable-line prettier/prettier /* Issued At */ if (!(0, number_1.isInt)(body.iat)) body.iat = now; /* Issuer (iss) */ if (typeof options.issuer === 'string') body.iss = options.issuer; /* Subject (sub) */ if (typeof options.subject === 'string' || Number.isFinite(options.subject)) body.sub = String(options.subject); /* Audience (aud) */ if (Array.isArray(options.audience)) body.aud = options.audience.length > 1 ? options.audience : options.audience[0]; else if (typeof options.audience === 'string') body.aud = options.audience; /* Expires In (exp) */ if ((0, number_1.isInt)(options.expiresIn)) { body.exp = now + options.expiresIn; } else if (options.expiresIn !== null) { body.exp = now + 3600; /* Default 1 hour expiry */ } /* Not Before (nbf) */ if ((0, number_1.isInt)(options.notBefore)) body.nbf = now + options.notBefore; /* JWT Id (jti) */ if (typeof options.jwtid === 'string') body.jti = options.jwtid; /* Create token */ const token = (0, Crypto_1.b64url)((0, Crypto_1.utf8Encode)(JSON.stringify(header))) + '.' + (0, Crypto_1.b64url)((0, Crypto_1.utf8Encode)(JSON.stringify(body))); if (header.alg === 'none') return token + '.'; /* Load up signing key */ const key = await (0, Crypto_1.importKey)(secret, Crypto_1.ALGOS[header.alg], ['sign']); /* Sign and concat onto token */ const sig = await crypto.subtle.sign(Crypto_1.ALGOS[header.alg], key, (0, Crypto_1.utf8Encode)(token)); return token + '.' + (0, Crypto_1.b64url)(new Uint8Array(sig)); } /** * Verifies a JWT and returns its decoded payload and header. * * @param {string} token - JWT string to verify * @param {string|JsonWebKey|CryptoKey} secret - secret or public key used to verify the signature * @param {JWTVerifyOptions} options - Verification constraints such as algorithm, audience, issuer, etc. * @returns The decoded JWT data if valid. * @throws JWTError subclasses on failure (e.g. JWTMalformedError, JWTTimeError, JWTSignatureError). * @see https://jwt.io */ async function jwtVerify(token, secret, options = {}) { const parts = token.split('.'); if (parts.length !== 3) throw new JWTMalformedError(); if (!(0, object_1.isObject)(options)) throw new Error('JWT@verify: Options must be provided'); const [raw_header, raw_payload, sig] = parts; if (!raw_header || !raw_payload) throw new JWTMalformedError(); const decoded = jwtDecode(token); const typ = options.type ?? 'JWT'; if (decoded._header.typ !== typ) throw new JWTTypeError(typ, decoded._header.typ); const now = Math.floor(Date.now() / 1000); const leeway = (0, number_1.isInt)(options.leeway) ? options.leeway : 0; /* Verify not-before */ if ((0, number_1.isInt)(decoded.nbf) && decoded.nbf > now + leeway) throw new JWTTimeError('NOT_YET_VALID'); /* Verify expiration */ if ((0, number_1.isInt)(decoded.exp) && decoded.exp <= now - leeway) throw new JWTTimeError('EXPIRED'); /* Verify issuer */ if (options.issuer && decoded.iss !== options.issuer) throw new JWTClaimError('ISSUER'); /* Verify audience */ if (options.audience) { const aud = Array.isArray(options.audience) ? options.audience : [options.audience]; const token_aud = Array.isArray(decoded.aud) ? decoded.aud : typeof decoded.aud === 'string' ? [decoded.aud] : []; if (!aud.some(a => token_aud.includes(a))) throw new JWTClaimError('AUDIENCE'); } /* Verify subject */ if (typeof options.subject === 'function' && (!decoded.sub || !options.subject(decoded.sub))) throw new JWTClaimError('SUBJECT'); /* Verify algorithm */ const alg = decoded._header.alg; const optalg = options.algorithm || 'HS256'; if (!alg || !(alg in Crypto_1.ALGOS)) throw new JWTAlgorithmError('UNSUPPORTED'); if (alg !== optalg) throw new JWTAlgorithmError('MISMATCH'); /* If alg is none, no need to continue verifying */ if (alg === 'none') return decoded; /* Verify signature exists */ if (!sig) throw new JWTMalformedError(); /* Validate signature */ try { const key = await (0, Crypto_1.importKey)(secret, Crypto_1.ALGOS[alg], ['verify']); const sig_valid = await crypto.subtle.verify(Crypto_1.ALGOS[alg], key, (0, Crypto_1.b64urlDecode)(sig), (0, Crypto_1.utf8Encode)(raw_header + '.' + raw_payload)); if (!sig_valid) throw new JWTSignatureError(); } catch (err) { throw new JWTSignatureError({ cause: err }); } return decoded; }