From e503b163e9c5265ed9a8c6a0e8638d007f907274 Mon Sep 17 00:00:00 2001 From: Toby Date: Thu, 22 Feb 2024 22:49:24 +0100 Subject: [PATCH] add clock tolerance --- src/index.ts | 18 ++++++++++++------ tests/index.spec.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index fae4723..afe5d36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,6 +99,11 @@ export type JwtSignOptions = { * @prop {boolean} [throwError=false] If `true` throw error if checks fail. (default: `false`) */ export type JwtVerifyOptions = { + /** + * Clock tolerance to help with slightly out of sync systems + */ + clockTolerance?: number + /** * If `true` throw error if checks fail. (default: `false`) * @@ -178,11 +183,10 @@ export async function sign(payload: JwtPayload} 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 | CryptoKey, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', throwError: false }): Promise { +export async function verify(token: string, secret: string | JsonWebKey | CryptoKey, options: JwtVerifyOptions | JwtAlgorithm = 'HS256'): Promise { if (typeof options === 'string') - options = { algorithm: options, throwError: false } - - options = { algorithm: 'HS256', throwError: false, ...options } + options = { algorithm: options } + options = { algorithm: 'HS256', clockTolerance: 0, throwError: false, ...options } if (typeof token !== 'string') throw new Error('token must be a string') @@ -215,10 +219,12 @@ export async function verify(token: string, secret: string | JsonWebKey | Crypto if (!payload) throw new Error('PARSE_ERROR') - if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1000)) + const now = Math.floor(Date.now() / 1000) + + if (payload.nbf && Math.abs(payload.nbf - now) > (options.clockTolerance ?? 0)) throw new Error('NOT_YET_VALID') - if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000)) + if (payload.exp && Math.abs(payload.exp - now) > (options.clockTolerance ?? 0)) throw new Error('EXPIRED') const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm, ['verify']) diff --git a/tests/index.spec.ts b/tests/index.spec.ts index 2579dd2..d9aa082 100644 --- a/tests/index.spec.ts +++ b/tests/index.spec.ts @@ -119,4 +119,30 @@ describe.each(Object.entries(data) as [JwtAlgorithm, Dataset][])('%s', (algorith const verified = await jwt.verify(token, data.public, algorithm) expect(verified).toBeTruthy() }) +}) + +describe('Verify', async () => { + const secret = 'super-secret' + + const now = Math.floor(Date.now() / 1000) + const off = 30 // 30 seconds + const nbf = now + off // Not valid before 30 seconds from now + const exp = now - off // Expired 30 seconds ago + + const notYetValidToken = await jwt.sign({ sub: 'me', nbf }, secret) + const expiredToken = await jwt.sign({ sub: 'me', exp }, secret) + + test('Not yet valid', () => { + expect(jwt.verify(notYetValidToken, secret, { throwError: true })).rejects.toThrowError('NOT_YET_VALID') + }) + + test('Expired', () => { + console.log({ exp, now: Math.floor(Date.now() / 1000) }) + expect(jwt.verify(expiredToken, secret, { throwError: true })).rejects.toThrowError('EXPIRED') + }) + + test('Clock offset', () => { + expect(jwt.verify(notYetValidToken, secret, { clockTolerance: off, throwError: true })).resolves.toBe(true) + expect(jwt.verify(expiredToken, secret, { clockTolerance: off, throwError: true })).resolves.toBe(true) + }) }) \ No newline at end of file