diff --git a/.changeset/wacky-dryers-hammer.md b/.changeset/wacky-dryers-hammer.md new file mode 100644 index 00000000000..6076445c4c1 --- /dev/null +++ b/.changeset/wacky-dryers-hammer.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +A clock skew of 0 will not fall back to the default value anymore. diff --git a/packages/backend/src/jwt/__tests__/verifyJwt.test.ts b/packages/backend/src/jwt/__tests__/verifyJwt.test.ts index 4fd4022a884..d2f529251d8 100644 --- a/packages/backend/src/jwt/__tests__/verifyJwt.test.ts +++ b/packages/backend/src/jwt/__tests__/verifyJwt.test.ts @@ -217,4 +217,56 @@ describe('verifyJwt(jwt, options)', () => { expect(error?.message).toContain('Invalid JWT type'); expect(error?.message).toContain('Expected "at+jwt, application/at+jwt"'); }); + + it('rejects an expired JWT when clockSkewInMs is explicitly 0', async () => { + vi.setSystemTime(new Date((mockJwtPayload.exp + 1) * 1000)); + const inputVerifyJwtOptions = { + key: mockJwks.keys[0], + issuer: mockJwtPayload.iss, + authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'], + clockSkewInMs: 0, + }; + const { errors: [error] = [] } = await verifyJwt(mockJwt, inputVerifyJwtOptions); + expect(error).toBeDefined(); + expect(error?.message).toContain('JWT is expired'); + }); + + it('accepts a recently expired JWT within the default clock skew when clockSkewInMs is undefined', async () => { + vi.setSystemTime(new Date((mockJwtPayload.exp + 1) * 1000)); + const inputVerifyJwtOptions = { + key: mockJwks.keys[0], + issuer: mockJwtPayload.iss, + authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'], + }; + const { data } = await verifyJwt(mockJwt, inputVerifyJwtOptions); + expect(data).toEqual(mockJwtPayload); + }); + + it('falls back to the default clock skew when clockSkewInMs is NaN', async () => { + vi.setSystemTime(new Date((mockJwtPayload.exp + 1) * 1000)); + const inputVerifyJwtOptions = { + key: mockJwks.keys[0], + issuer: mockJwtPayload.iss, + authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'], + clockSkewInMs: Number.NaN, + }; + const { data } = await verifyJwt(mockJwt, inputVerifyJwtOptions); + expect(data).toEqual(mockJwtPayload); + + vi.setSystemTime(new Date((mockJwtPayload.exp + 60) * 1000)); + const { errors: [error] = [] } = await verifyJwt(mockJwt, inputVerifyJwtOptions); + expect(error?.message).toContain('JWT is expired'); + }); + + it('falls back to the default clock skew when clockSkewInMs is Infinity', async () => { + vi.setSystemTime(new Date((mockJwtPayload.exp + 3600) * 1000)); + const inputVerifyJwtOptions = { + key: mockJwks.keys[0], + issuer: mockJwtPayload.iss, + authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'], + clockSkewInMs: Number.POSITIVE_INFINITY, + }; + const { errors: [error] = [] } = await verifyJwt(mockJwt, inputVerifyJwtOptions); + expect(error?.message).toContain('JWT is expired'); + }); }); diff --git a/packages/backend/src/jwt/verifyJwt.ts b/packages/backend/src/jwt/verifyJwt.ts index 5b828bd8a18..b96055126c4 100644 --- a/packages/backend/src/jwt/verifyJwt.ts +++ b/packages/backend/src/jwt/verifyJwt.ts @@ -131,7 +131,8 @@ export async function verifyJwt( options: VerifyJwtOptions, ): Promise> { const { audience, authorizedParties, clockSkewInMs, key, headerType } = options; - const clockSkew = clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_MS; + const clockSkew = + typeof clockSkewInMs === 'number' && Number.isFinite(clockSkewInMs) ? clockSkewInMs : DEFAULT_CLOCK_SKEW_IN_MS; const { data: decoded, errors } = decodeJwt(token); if (errors) {