@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
216 lines (215 loc) • 9.01 kB
JavaScript
;
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;
}