diff --git a/src/index.spec.ts b/src/index.spec.ts index b02ac5c..3520b0d 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -82,7 +82,7 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith sub: payload.sub, name: payload.name }).toMatchObject({ - sub: decoded.payload.sub, + sub: decoded.payload?.sub, name: payload.name }) }) @@ -98,7 +98,7 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith sub: payload.sub, name: payload.name }).toMatchObject({ - sub: decoded.payload.sub, + sub: decoded.payload?.sub, name: payload.name }) }) diff --git a/src/index.ts b/src/index.ts index fe7fe6b..65c2a5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,11 +87,6 @@ export interface JwtSignOptions extends JwtOptions { * @prop {boolean} [throwError=false] If `true` throw error if checks fail. (default: `false`) */ export interface JwtVerifyOptions extends JwtOptions { - /** - * If `true` all expiry checks will be skipped - */ - skipValidation?: boolean - /** * If `true` throw error if checks fail. (default: `false`) * @@ -106,8 +101,8 @@ export interface JwtVerifyOptions extends JwtOptions { * @prop {JwtPayload} payload */ export interface JwtData { - header: JwtHeader - payload: JwtPayload + header?: JwtHeader + payload?: JwtPayload } const algorithms: JwtAlgorithms = { @@ -203,77 +198,14 @@ async function importKey(key: string | JsonWebKey, algorithm: SubtleCryptoImport return importTextSecret(key, algorithm) } -function decodePayload(raw: string): JwtHeader | JwtPayload | null { +function decodePayload(raw: string): T | undefined { try { - raw += '='.repeat(4-(raw.length % 4)) - return JSON.parse(atob(raw)) + return JSON.parse(atob(raw)) } catch { - return null + return } } -/** - * Verifies the integrity of the token and returns a boolean value. - * - * @param {string} token The token string generated by `jwt.sign()`. - * @param {string | JsonWebKey} secret The string which was used to sign the payload. - * @param {JWTVerifyOptions | JWTAlgorithm} 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`. - */ -export async function verify(token: string, secret: string | JsonWebKey, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', skipValidation: false, throwError: false }): Promise { - if (typeof options === 'string') - options = { algorithm: options, throwError: false } - - options = { algorithm: 'HS256', skipValidation: false, throwError: false, ...options } - - if (typeof token !== 'string') - throw new Error('token must be a string') - - if (typeof secret !== 'string' && typeof secret !== 'object') - throw new Error('secret must be a string or a JWK object') - - if (typeof options.algorithm !== 'string') - throw new Error('options.algorithm must be a string') - - const tokenParts = token.split('.') - - if (tokenParts.length !== 3) - throw new Error('token must consist of 3 parts') - - const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm] - - if (!algorithm) - throw new Error('algorithm not found') - - const { payload } = decode(token) - - if (!options.skipValidation && !payload) { - if (options.throwError) - throw 'PARSE_ERROR' - - return false - } - - if (!options.skipValidation && payload.nbf && payload.nbf > Math.floor(Date.now() / 1000)) { - if (options.throwError) - throw 'NOT_YET_VALID' - - return false - } - - if (!options.skipValidation && payload.exp && payload.exp <= Math.floor(Date.now() / 1000)) { - if (options.throwError) - throw 'EXPIRED' - - return false - } - - const key = await importKey(secret, algorithm) - - return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`)) -} - /** * Signs a payload and returns the token * @@ -286,6 +218,7 @@ export async function verify(token: string, secret: string | JsonWebKey, options export async function sign(payload: JwtPayload, secret: string | JsonWebKey, options: JwtSignOptions | JwtAlgorithm = 'HS256'): Promise { if (typeof options === 'string') options = { algorithm: options } + options = { algorithm: 'HS256', header: { typ: 'JWT' }, ...options } if (!payload || typeof payload !== 'object') @@ -313,6 +246,63 @@ export async function sign(payload: JwtPayload, secret: string | JsonWebKey, opt return `${partialToken}.${arrayBufferToBase64Url(signature)}` } +/** + * Verifies the integrity of the token and returns a boolean value. + * + * @param {string} token The token string generated by `jwt.sign()`. + * @param {string | JsonWebKey} secret The string which was used to sign the payload. + * @param {JWTVerifyOptions | JWTAlgorithm} 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`. + */ +export async function verify(token: string, secret: string | JsonWebKey, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', throwError: false }): Promise { + if (typeof options === 'string') + options = { algorithm: options, throwError: false } + + options = { algorithm: 'HS256', throwError: false, ...options } + + if (typeof token !== 'string') + throw new Error('token must be a string') + + if (typeof secret !== 'string' && typeof secret !== 'object') + throw new Error('secret must be a string or a JWK object') + + if (typeof options.algorithm !== 'string') + throw new Error('options.algorithm must be a string') + + const tokenParts = token.split('.') + + if (tokenParts.length !== 3) + throw new Error('token must consist of 3 parts') + + const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm] + + if (!algorithm) + throw new Error('algorithm not found') + + try { + const { payload } = decode(token) + + if (!payload) + throw new Error('PARSE_ERROR') + + if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1000)) + throw new Error('NOT_YET_VALID') + + if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000)) + throw new Error('EXPIRED') + + const key = await importKey(secret, algorithm) + + return await crypto.subtle.verify(algorithm, key, base64UrlToArrayBuffer(tokenParts[2]), textToArrayBuffer(`${tokenParts[0]}.${tokenParts[1]}`)) + } catch(err) { + if (options.throwError) + throw err + + return false + } +} + /** * Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure! * @@ -321,8 +311,8 @@ export async function sign(payload: JwtPayload, secret: string | JsonWebKey, opt */ export function decode(token: string): JwtData { return { - header: decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')) as JwtHeader, - payload: decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) as JwtPayload + header: decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')), + payload: decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) } }