UNPKG

nomatic-jwt

Version:

JSON Web Token (JWT) utilities for Node.js

358 lines (287 loc) 10.3 kB
import * as crypto from 'crypto'; import * as CryptoJS from 'crypto-js'; import * as base64 from './base64'; import {JWTError, JWTExpiredError, JWTSignatureError} from './errors'; export type JWTAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512'; export type JWTClaims = JWTRegisteredClaims & JWTPrivateClaims; export type JWTPayloadData = JWTClaims | string; export interface JWTHeaderData { typ: string; alg: string; } export interface JWTOptions { algorithm?: JWTAlgorithm; autoValidate?: boolean; expiresIn?: number; // In seconds timeOffset?: number; // In seconds key?: string; privateKey?: string; publicKey?: string; } export interface JWTData { header: JWTHeaderData; payload: JWTPayloadData; signature: string; } export interface JWTRegisteredClaims { /** * Issuer (registered): RFC-7519 (https://tools.ietf.org/html/rfc7519#section-4.1.1) */ iss?: any; /** * Subject (registered): RFC-7519 (https://tools.ietf.org/html/rfc7519#section-4.1.2) */ sub?: any; /** * Audience (registered): RFC-7519 (https://tools.ietf.org/html/rfc7519#section-4.1.3) */ aud?: any; /** * Expiration Time (registered): RFC-7519 (https://tools.ietf.org/html/rfc7519#section-4.1.4) */ exp?: number; /** * Not Before (registered): RFC-7519 (https://tools.ietf.org/html/rfc7519#section-4.1.5) */ nbf?: number; /** * Issued At (registered): RFC-7519 (https://tools.ietf.org/html/rfc7519#section-4.1.6) */ iat?: number; // In seconds /** * JWT ID (registered): RFC-7519 (https://tools.ietf.org/html/rfc7519#section-4.1.7) */ jid?: any; } export interface JWTPrivateClaims { [key: string]: string | number | boolean | Object | any[]; } export class JWT { private _algorithm: JWTAlgorithm; private _autoValidate: boolean; private _expiresIn: number; private _key: string; private _privateKey: string; private _publicKey: string; private _timeOffset: number; constructor (options: JWTOptions = {}) { this.algorithm = options.algorithm || 'HS256'; this.expiresIn = options.expiresIn || (60 * 60); this.autoValidate = options.autoValidate || true; if (this.algorithm.startsWith('HS')) { this.key = options.key || crypto.randomBytes(128).toString('hex'); } else if (options.privateKey && options.publicKey) { this.privateKey = options.privateKey; this.publicKey = options.publicKey; } else { throw new JWTError(`'privateKey' and 'publicKey' must be specified with ${this.algorithm} algorithm`); } this.timeOffset = options.timeOffset || 60; } public static parsePayload(payload: string): JWTPayloadData { try { return JSON.parse(payload); } catch (error) { if (error.name !== 'SyntaxError') { throw error; } else { return payload; } } } public get algorithm() { return this._algorithm; } public set algorithm(algorithm: JWTAlgorithm) { if (algorithm.startsWith('HS') || algorithm.startsWith('RS')) { if (algorithm.endsWith('256') || algorithm.endsWith('384') || algorithm.endsWith('512')) { this._algorithm = algorithm; return; } } throw new JWTError(`Invalid algorithm: ${algorithm}`); } public get autoValidate() { return this._autoValidate; } public set autoValidate(autoValidate: boolean) { this._autoValidate = autoValidate; } public get expiresIn() { return this._expiresIn; } public set expiresIn(expiresIn: number) { if (typeof expiresIn === 'number') { this._expiresIn = expiresIn; return; } throw new JWTError('\'expiresIn\' must be a number'); } public get key() { return this._key; } public set key(key: string) { if (typeof key === 'string') { this._key = key; return; } throw new JWTError('\'key\' must be a string'); } public get privateKey() { return this._privateKey; } public set privateKey(privateKey: string) { if (typeof privateKey === 'string') { this._privateKey = privateKey; return; } throw new JWTError('\'privateKey\' must be a string'); } public get publicKey() { return this._publicKey; } public set publicKey(publicKey: string) { if (typeof publicKey === 'string') { this._publicKey = publicKey; return; } throw new JWTError('\'publicKey\' must be a string'); } public get timeOffset() { return this._timeOffset; } public set timeOffset(timeOffset: number) { if (typeof timeOffset === 'number') { this._timeOffset = timeOffset; return; } throw new JWTError('\'timeOffset\' must be a number'); } public signRaw(data: string, key: string = null, algorithm: JWTAlgorithm = this.algorithm): string { if (!key) { if (algorithm.startsWith('RS')) { key = this.privateKey; } else if (algorithm.startsWith('HS')) { key = this.key; } } let signature: string; if (algorithm.startsWith('RS')) { signature = crypto.createSign('RSA-SHA' + algorithm.substr(2)) .update(data) .sign(key, 'base64'); } else if (algorithm.startsWith('HS')) { signature = CryptoJS .enc .Base64 .stringify(CryptoJS['HmacSHA' + algorithm.substr(2)](data, key)); } else { throw new Error('Unknown or unsupported algorithm: ' + algorithm); } return base64.escape(signature); } public verifyRaw(data: string, signature: string, key: string = null, algorithm: JWTAlgorithm = this.algorithm): boolean { if (algorithm.startsWith('HS')) { if (!key) { key = this.key; } return signature === this.signRaw(data, key, algorithm); } else if (algorithm.startsWith('RS')) { if (!key) { key = this.publicKey; } return crypto.createVerify('RSA-SHA' + algorithm.substr(2)) .update(data) .verify(key, base64.unescape(signature), 'base64'); } else { throw new Error('Unknown or unsupported algorithm: ' + algorithm); } } public decode(encoded: string, key: string = null, algorithm: JWTAlgorithm = this.algorithm): JWTData { if (!key) { if (algorithm.startsWith('RS')) { key = this.publicKey; } else if (algorithm.startsWith('HS')) { key = this.key; } } const encodedParts = encoded.split('.'); if (encodedParts.length !== 3) { throw new Error('Invalid number of encoded parts: ' + encoded.length); } const token: JWTData = { header: JSON.parse(base64.decodeSafe(encodedParts[0])), payload: JWT.parsePayload(base64.decodeSafe(encodedParts[1])), signature: encodedParts[2] }; if (this.autoValidate) { return this.validate(token, key, algorithm); } else { return token; } } public encode(payload: JWTPayloadData, key: string = null, algorithm: JWTAlgorithm = this.algorithm): string { if (!key) { if (algorithm.startsWith('RS')) { key = this.privateKey; } else if (algorithm.startsWith('HS')) { key = this.key; } } const header: JWTHeaderData = { typ: 'JWT', alg: algorithm }; if (this.expiresIn && !(payload instanceof String) && !payload.exp) { const current = Math.floor(( new Date().getTime() / 1000)); payload.exp = current + this.expiresIn; if (!payload.nbf) { payload.nbf = current; } if (!payload.iat) { payload.iat = current; } } const encoded = []; encoded.push(base64.encodeSafe(JSON.stringify(header))); encoded.push(base64.encodeSafe(JSON.stringify(payload))); encoded.push(this.signRaw(encoded.join('.'), key, algorithm)); return encoded.join('.'); } public validate(token: JWTData, key: string = null, algorithm: JWTAlgorithm = this.algorithm): JWTData { if (!key) { if (algorithm.startsWith('RS')) { key = this.publicKey; } else if (algorithm.startsWith('HS')) { key = this.key; } } const encoded = []; encoded.push(base64.encodeSafe(JSON.stringify(token.header))); encoded.push(base64.encodeSafe(JSON.stringify(token.payload))); const data = encoded.join('.'); if (!(this.verifyRaw(data, token.signature, key, algorithm))) { throw new JWTSignatureError(); } if (!(token.payload instanceof String)) { // `this.timeOffset`, `token.payload['nbf']` (not before) and `token.payload['exp']` (expires) are in seconds if (token.payload['nbf']) { const current = Math.floor((Date.now() / 1000)); if (token.payload['nbf'] > (current + this.timeOffset)) { throw new JWTError('JWT is not active yet'); } } if (token.payload['exp']) { const current = Math.floor((Date.now() / 1000)); if (current + this.timeOffset > token.payload['exp']) { throw new JWTExpiredError(token.payload['exp']); } } } return token; } } /** * @module nomatic-jwt */ export default new JWT();