UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions, classes and types for everyday development needs.

170 lines (169 loc) 7.07 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Signet = void 0; const helpers_1 = require("../date/helpers"); const parse_1 = require("../date/parse"); const non_primitives_1 = require("../guards/non-primitives"); const primitives_1 = require("../guards/primitives"); const index_1 = require("../utils/index"); const helpers_2 = require("./helpers"); const utils_1 = require("./utils"); class Signet { #secretBytes; constructor(secret) { if (!(0, primitives_1.isNonEmptyString)(secret)) { throw new Error('Secret must be a non-empty string!'); } this.#secretBytes = (0, utils_1.utf8ToBytes)(secret); } #decode(token) { if (!(0, primitives_1.isNonEmptyString)(token)) { throw new Error('Token must be a non-empty string!'); } const parts = token.split('.'); if (parts.length !== 3) { throw new Error('Token is tampered or malformed!'); } const [hdr, pld, signature] = parts; const headerBytes = (0, utils_1.base64ToBytes)(hdr); const payloadBytes = (0, utils_1.base64ToBytes)(pld); const headerStr = (0, index_1.stripJsonEdgeGarbage)((0, utils_1.bytesToUtf8)(headerBytes)); const payloadStr = (0, index_1.stripJsonEdgeGarbage)((0, utils_1.bytesToUtf8)(payloadBytes)); let header; try { header = JSON.parse(headerStr); } catch { throw new Error('Cannot parse header!'); } let payload; try { const { iat, iatDate, exp, expDate, nbf, nbfDate, aud, sub, iss, ...rest } = JSON.parse(payloadStr); payload = { iat, iatDate: iatDate ? new Date(iatDate) : (0, helpers_1._secToDate)(iat), ...(exp && { exp }), ...(exp && { expDate: expDate ? new Date(expDate) : (0, helpers_1._secToDate)(exp) }), ...(nbf && { nbf }), ...(nbf && { nbfDate: nbfDate ? new Date(nbfDate) : (0, helpers_1._secToDate)(nbf) }), ...(aud && { aud }), ...(sub && { sub }), ...(iss && { iss }), ...rest, }; } catch { throw new Error('Cannot parse payload!'); } return { header, payload, signature, signingInput: `${hdr}.${pld}`, }; } sign(payload, options) { if (!(0, non_primitives_1.isNotEmptyObject)(payload)) throw new Error('Payload must be a valid object!'); const { expiresIn, notBefore, audience, issuer, subject } = options || {}; const iat = (0, helpers_1._toSeconds)(Date.now()); const $payload = { iat, iatDate: (0, helpers_1._secToDate)(iat), ...(expiresIn && { exp: iat + (0, helpers_1._toSeconds)((0, parse_1.parseMSec)(expiresIn)) }), ...(expiresIn && { expDate: (0, helpers_1._secToDate)(iat + (0, helpers_1._toSeconds)((0, parse_1.parseMSec)(expiresIn))) }), ...(notBefore && { nbf: iat + (0, helpers_1._toSeconds)((0, parse_1.parseMSec)(notBefore)) }), ...(notBefore && { nbfDate: (0, helpers_1._secToDate)(iat + (0, helpers_1._toSeconds)((0, parse_1.parseMSec)(notBefore))) }), ...(audience && { aud: audience }), ...(subject && { sub: subject }), ...(issuer && { iss: issuer }), ...payload, }; const header = { alg: 'HS256', typ: 'SIGNET+JWT' }; const headerJson = (0, index_1.stableStringify)(header); const payloadJson = (0, index_1.stableStringify)($payload); const headerB = (0, utils_1.utf8ToBytes)(headerJson); const payloadB = (0, utils_1.utf8ToBytes)(payloadJson); const signingInput = `${(0, utils_1.bytesToBase64)(headerB)}.${(0, utils_1.bytesToBase64)(payloadB)}`; const mac = (0, utils_1.hmacSha256)(this.#secretBytes, (0, utils_1.utf8ToBytes)(signingInput)); const signature = (0, utils_1.bytesToBase64)(mac); return `${signingInput}.${signature}`; } decode(token) { return this.#decode(token); } hasExpired(token) { const { exp } = this.#decode(token).payload; return exp ? (0, helpers_1._toSeconds)(Date.now()) > exp : false; } isTooEarly(token) { const { nbf } = this.#decode(token).payload; return nbf ? (0, helpers_1._toSeconds)(Date.now()) < nbf : false; } isInvalidIssuer(token, expected) { if (!expected) return false; const { iss } = this.#decode(token).payload; return iss ? iss !== expected : false; } isInvalidAudience(token, expected) { if (!expected) return false; const { aud } = this.#decode(token).payload; if (!aud) return false; const payloadAud = Array.isArray(aud) ? aud : [aud]; const expectedAud = Array.isArray(expected) ? expected : [expected]; return !payloadAud.some((tokenAud) => expectedAud.includes(tokenAud)); } isInvalidSubject(token, expected) { if (!expected) return false; const { sub } = this.#decode(token).payload; return sub ? sub !== expected : false; } verify(token, options) { try { const { signature, signingInput, payload } = this.#decode(token); const { audience, issuer, subject } = options || {}; const expectedMac = (0, utils_1.hmacSha256)(this.#secretBytes, (0, utils_1.utf8ToBytes)(signingInput)); const expectedSig = (0, utils_1.bytesToBase64)(expectedMac); if (!(0, helpers_2._constantTimeEquals)(signature, expectedSig)) { throw new Error('Invalid or tampered signature!'); } if (this.hasExpired(token)) { throw new Error('Token has expired!'); } if (this.isTooEarly(token)) { throw new Error('Token is not active yet!'); } if (this.isInvalidIssuer(token, issuer)) { throw new Error('Invalid token issuer!'); } if (this.isInvalidAudience(token, audience)) { throw new Error('Invalid token audience(s)!'); } if (this.isInvalidSubject(token, subject)) { throw new Error('Invalid token subject!'); } return { isValid: true, payload }; } catch (e) { return { isValid: false, error: e instanceof Error ? e.message : String(e), }; } } verifyOrThrow(token, options) { const res = this.verify(token, options); if (!res.isValid) { throw new Error(res.error || 'Invalid, malformed or expired token!'); } return res; } decodePayload(token) { return this.#decode(token).payload; } } exports.Signet = Signet;