From 6dbd703d03a6c7d14e576bcf9d818451b972e109 Mon Sep 17 00:00:00 2001 From: Tobias Schneider Date: Sun, 26 Jun 2022 15:54:10 +0200 Subject: [PATCH] clean up --- src/index.ts | 106 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2f0ea44..2112820 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,14 @@ +/** + * @typedef JwtAlgorithms + */ interface JwtAlgorithms { [key: string]: SubtleCryptoImportKeyAlgorithm } +/** + * @typedef JwtAlgorithm + * @enum {string} + */ enum JwtAlgorithm { ES256 = 'ES256', ES384 = 'ES384', @@ -14,33 +21,68 @@ enum JwtAlgorithm { RS512 = 'RS512' } +/** + * @typedef JwtHeader + * @prop {string} [typ] Type + */ interface JwtHeader { - typ?: string, + /** + * Type (default: `"JWT"`) + * + * @default "JWT" + */ + typ?: string + [key: string]: any } +/** + * @typedef JwtPayload + * @prop {string} [iss] Issuer + * @prop {string} [sub] Subject + * @prop {string} [aud] Audience + * @prop {string} [exp] Expiration Time + * @prop {string} [nbf] Not Before + * @prop {string} [iat] Issued At + * @prop {string} [jti] JWT ID + */ interface JwtPayload { + /** Issuer */ iss?: string + + /** Subject */ sub?: string + + /** Audience */ aud?: string + + /** Expiration Time */ exp?: number + + /** Not Before */ nbf?: number + + /** Issued At */ iat?: number + + /** JWT ID */ jti?: string + [key: string]: any } /** * @typedef JwtOptions - * @property {JwtAlgorithm} algorithm + * @prop {JwtAlgorithm | string} algorithm */ interface JwtOptions { - algorithm: JwtAlgorithm + algorithm?: JwtAlgorithm | string } /** * @typedef JwtSignOptions - * @property {JwtHeader} [header] + * @extends JwtOptions + * @prop {JwtHeader} [header] */ interface JwtSignOptions extends JwtOptions { header?: JwtHeader @@ -48,28 +90,49 @@ interface JwtSignOptions extends JwtOptions { /** * @typedef JwtVerifyOptions - * @property {boolean} [throwError] + * @extends JwtOptions + * @prop {boolean=false} [throwError] If `true` throw error if checks fail. (default: `false`) */ interface JwtVerifyOptions extends JwtOptions { + /** + * If `true` throw error if checks fail. (default: `false`) + * + * @default false + */ throwError?: boolean } +/** + * @typedef JwtData + * @prop {JwtHeader} header + * @prop {JwtPayload} payload + */ interface JwtData { - header: JwtHeader | null - payload: JwtPayload | null + header: JwtHeader + payload: JwtPayload } /** - * Base64URL + * Base64Url * + * @public * @class */ - class Base64URL { +class Base64Url { + /** + * @param {string} s + * @returns {Uint8Array} + */ public static parse(s: string): Uint8Array { // @ts-ignore return new Uint8Array(Array.prototype.map.call(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), c => c.charCodeAt(0))) // return new Uint8Array(Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''))).map(c => c.charCodeAt(0))) } + + /** + * @param {Uint8Array} a + * @returns {string} + */ public static stringify(a: Uint8Array): string { // @ts-ignore return btoa(String.fromCharCode.apply(0, a)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') @@ -80,9 +143,8 @@ interface JwtData { /** * Jwt * - * @class - * @constructor * @public + * @class */ class Jwt { @@ -104,7 +166,7 @@ class Jwt { } protected _utf8ToUint8Array(str: string): Uint8Array { - return Base64URL.parse(btoa(unescape(encodeURIComponent(str)))) + return Base64Url.parse(btoa(unescape(encodeURIComponent(str)))) } protected _str2ab(str: string): ArrayBuffer { @@ -117,7 +179,7 @@ class Jwt { return buf; } - protected _decodePayload(raw: string): any { + protected _decodePayload(raw: string): JwtHeader | JwtPayload | null { switch (raw.length % 4) { case 0: break @@ -142,14 +204,13 @@ class Jwt { * * @param {JwtPayload} payload The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload. * @param {string} secret A string which is used to sign the payload. - * @param {JwtSignOptions | JwtAlgorithm} [options={ algorithm: 'HS256', header: { typ: 'JWT' } }] The options object or the algorithm. + * @param {JwtSignOptions | JwtAlgorithm | string} [options={ algorithm: 'HS256', header: { typ: 'JWT' } }] The options object or the algorithm. * @throws {Error} If there's a validation issue. * @returns {Promise} Returns token as a `string`. */ public async sign(payload: JwtPayload, secret: string, options: JwtSignOptions | JwtAlgorithm = { algorithm: JwtAlgorithm.HS256, header: { typ: 'JWT' } }): Promise { if (typeof options === 'string') options = { algorithm: options, header: { typ: 'JWT' } } - // @ts-ignore options = { algorithm: JwtAlgorithm.HS256, header: { typ: 'JWT' }, ...options } if (payload === null || typeof payload !== 'object') throw new Error('payload must be an object') @@ -162,7 +223,7 @@ class Jwt { throw new Error('algorithm not found') payload.iat = Math.floor(Date.now() / 1000) const payloadAsJSON = JSON.stringify(payload) - const partialToken = `${Base64URL.stringify(this._utf8ToUint8Array(JSON.stringify({ ...options.header, alg: options.algorithm })))}.${Base64URL.stringify(this._utf8ToUint8Array(payloadAsJSON))}` + const partialToken = `${Base64Url.stringify(this._utf8ToUint8Array(JSON.stringify({ ...options.header, alg: options.algorithm })))}.${Base64Url.stringify(this._utf8ToUint8Array(payloadAsJSON))}` let keyFormat = 'raw' let keyData if (secret.startsWith('-----BEGIN')) { @@ -172,7 +233,7 @@ class Jwt { keyData = this._utf8ToUint8Array(secret) const key = await crypto.subtle.importKey(keyFormat, keyData, algorithm, false, ['sign']) const signature = await crypto.subtle.sign(algorithm, key, this._utf8ToUint8Array(partialToken)) - return `${partialToken}.${Base64URL.stringify(new Uint8Array(signature))}` + return `${partialToken}.${Base64Url.stringify(new Uint8Array(signature))}` } /** @@ -180,14 +241,13 @@ class Jwt { * * @param {string} token The token string generated by `jwt.sign()`. * @param {string} secret The string which was used to sign the payload. - * @param {JWTVerifyOptions | JWTAlgorithm} options The options object or the algorithm. + * @param {JWTVerifyOptions | JWTAlgorithm | string} options The options object or the algorithm. * @throws {Error | string} Throws an error `string` if the token is invalid or an `Error-Object` if there's a validation issue. * @returns {Promise} Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`. */ - async verify(token: string, secret: string, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: JwtAlgorithm.ES256, throwError: false }): Promise { + async verify(token: string, secret: string, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: JwtAlgorithm.HS256, throwError: false }): Promise { if (typeof options === 'string') options = { algorithm: options, throwError: false } - // @ts-ignore options = { algorithm: JwtAlgorithm.HS256, throwError: false, ...options } if (typeof token !== 'string') throw new Error('token must be a string') @@ -225,7 +285,7 @@ class Jwt { } else keyData = this._utf8ToUint8Array(secret) const key = await crypto.subtle.importKey(keyFormat, keyData, algorithm, false, ['verify']) - return await crypto.subtle.verify(algorithm, key, Base64URL.parse(tokenParts[2]), this._utf8ToUint8Array(`${tokenParts[0]}.${tokenParts[1]}`)) + return await crypto.subtle.verify(algorithm, key, Base64Url.parse(tokenParts[2]), this._utf8ToUint8Array(`${tokenParts[0]}.${tokenParts[1]}`)) } /** @@ -236,8 +296,8 @@ class Jwt { */ public decode(token: string): JwtData { return { - header: this._decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')), - payload: this._decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) + header: this._decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')) as JwtHeader, + payload: this._decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) as JwtPayload } } }