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
JavaScript
;
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;