Skip to content

Commit abaa339

Browse files
fix(backend): Clock skew of 0 should not fall back (#8359)
1 parent 683399a commit abaa339

3 files changed

Lines changed: 59 additions & 1 deletion

File tree

.changeset/wacky-dryers-hammer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/backend': patch
3+
---
4+
5+
A clock skew of 0 will not fall back to the default value anymore.

packages/backend/src/jwt/__tests__/verifyJwt.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,56 @@ describe('verifyJwt(jwt, options)', () => {
217217
expect(error?.message).toContain('Invalid JWT type');
218218
expect(error?.message).toContain('Expected "at+jwt, application/at+jwt"');
219219
});
220+
221+
it('rejects an expired JWT when clockSkewInMs is explicitly 0', async () => {
222+
vi.setSystemTime(new Date((mockJwtPayload.exp + 1) * 1000));
223+
const inputVerifyJwtOptions = {
224+
key: mockJwks.keys[0],
225+
issuer: mockJwtPayload.iss,
226+
authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'],
227+
clockSkewInMs: 0,
228+
};
229+
const { errors: [error] = [] } = await verifyJwt(mockJwt, inputVerifyJwtOptions);
230+
expect(error).toBeDefined();
231+
expect(error?.message).toContain('JWT is expired');
232+
});
233+
234+
it('accepts a recently expired JWT within the default clock skew when clockSkewInMs is undefined', async () => {
235+
vi.setSystemTime(new Date((mockJwtPayload.exp + 1) * 1000));
236+
const inputVerifyJwtOptions = {
237+
key: mockJwks.keys[0],
238+
issuer: mockJwtPayload.iss,
239+
authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'],
240+
};
241+
const { data } = await verifyJwt(mockJwt, inputVerifyJwtOptions);
242+
expect(data).toEqual(mockJwtPayload);
243+
});
244+
245+
it('falls back to the default clock skew when clockSkewInMs is NaN', async () => {
246+
vi.setSystemTime(new Date((mockJwtPayload.exp + 1) * 1000));
247+
const inputVerifyJwtOptions = {
248+
key: mockJwks.keys[0],
249+
issuer: mockJwtPayload.iss,
250+
authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'],
251+
clockSkewInMs: Number.NaN,
252+
};
253+
const { data } = await verifyJwt(mockJwt, inputVerifyJwtOptions);
254+
expect(data).toEqual(mockJwtPayload);
255+
256+
vi.setSystemTime(new Date((mockJwtPayload.exp + 60) * 1000));
257+
const { errors: [error] = [] } = await verifyJwt(mockJwt, inputVerifyJwtOptions);
258+
expect(error?.message).toContain('JWT is expired');
259+
});
260+
261+
it('falls back to the default clock skew when clockSkewInMs is Infinity', async () => {
262+
vi.setSystemTime(new Date((mockJwtPayload.exp + 3600) * 1000));
263+
const inputVerifyJwtOptions = {
264+
key: mockJwks.keys[0],
265+
issuer: mockJwtPayload.iss,
266+
authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'],
267+
clockSkewInMs: Number.POSITIVE_INFINITY,
268+
};
269+
const { errors: [error] = [] } = await verifyJwt(mockJwt, inputVerifyJwtOptions);
270+
expect(error?.message).toContain('JWT is expired');
271+
});
220272
});

packages/backend/src/jwt/verifyJwt.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ export async function verifyJwt(
131131
options: VerifyJwtOptions,
132132
): Promise<JwtReturnType<JwtPayload, TokenVerificationError>> {
133133
const { audience, authorizedParties, clockSkewInMs, key, headerType } = options;
134-
const clockSkew = clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_MS;
134+
const clockSkew =
135+
typeof clockSkewInMs === 'number' && Number.isFinite(clockSkewInMs) ? clockSkewInMs : DEFAULT_CLOCK_SKEW_IN_MS;
135136

136137
const { data: decoded, errors } = decodeJwt(token);
137138
if (errors) {

0 commit comments

Comments
 (0)