Skip to content

Commit 62cab9e

Browse files
authored
test(clerk-js): Add dynamic TTL calculation tests for JWT expiration handling (#6231)
1 parent bcca299 commit 62cab9e

File tree

2 files changed

+109
-1
lines changed

2 files changed

+109
-1
lines changed

.changeset/ten-kiwis-cry.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
---
3+

packages/clerk-js/src/core/__tests__/tokenCache.spec.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { TokenResource } from '@clerk/types';
2-
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
2+
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
33

44
import { Token } from '../resources/internal';
55
import { SessionTokenCache } from '../tokenCache';
@@ -16,6 +16,30 @@ vi.mock('../resources/Base', () => {
1616
const jwt =
1717
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzU4NzY3OTAsImRhdGEiOiJmb29iYXIiLCJpYXQiOjE2NzU4NzY3MzB9.Z1BC47lImYvaAtluJlY-kBo0qOoAk42Xb-gNrB2SxJg';
1818

19+
// Helper function to create JWT with custom exp and iat values using the same structure as the working JWT
20+
function createJwtWithTtl(ttlSeconds: number): string {
21+
// Use the existing JWT as template
22+
const baseJwt =
23+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzU4NzY3OTAsImRhdGEiOiJmb29iYXIiLCJpYXQiOjE2NzU4NzY3MzB9.Z1BC47lImYvaAtluJlY-kBo0qOoAk42Xb-gNrB2SxJg';
24+
const [headerB64, , signature] = baseJwt.split('.');
25+
26+
// Use the same iat as the original working JWT to maintain consistency with test environment
27+
// Original JWT: iat: 1675876730, exp: 1675876790 (60 second TTL)
28+
const baseIat = 1675876730;
29+
const payload = {
30+
exp: baseIat + ttlSeconds,
31+
data: 'foobar', // Keep same data as original
32+
iat: baseIat,
33+
};
34+
35+
// Encode the new payload using base64url encoding (like JWT standard)
36+
const payloadString = JSON.stringify(payload);
37+
// Use proper base64url encoding: standard base64 but replace + with -, / with _, and remove padding =
38+
const newPayloadB64 = btoa(payloadString).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
39+
40+
return `${headerB64}.${newPayloadB64}.${signature}`;
41+
}
42+
1943
describe('MemoryTokenCache', () => {
2044
beforeAll(() => {
2145
vi.useFakeTimers();
@@ -163,4 +187,85 @@ describe('MemoryTokenCache', () => {
163187
expect(cache.get(key, 0)).toBeUndefined();
164188
});
165189
});
190+
191+
describe('dynamic TTL calculation', () => {
192+
let dateNowSpy: ReturnType<typeof vi.spyOn>;
193+
194+
afterEach(() => {
195+
dateNowSpy.mockRestore();
196+
});
197+
198+
it('calculates expiresIn from JWT exp and iat claims and sets timeout based on calculated TTL', async () => {
199+
const cache = SessionTokenCache;
200+
201+
// Mock Date.now to return a fixed timestamp initially
202+
const initialTime = 1675876730000; // Same as our JWT's iat in milliseconds
203+
dateNowSpy = vi.spyOn(Date, 'now').mockImplementation(() => initialTime);
204+
205+
// Test with a 30-second TTL
206+
const shortTtlJwt = createJwtWithTtl(30);
207+
const shortTtlToken = new Token({
208+
object: 'token',
209+
id: 'short-ttl',
210+
jwt: shortTtlJwt,
211+
});
212+
213+
const shortTtlKey = { tokenId: 'short-ttl', audience: 'test' };
214+
const shortTtlResolver = Promise.resolve(shortTtlToken);
215+
cache.set({ ...shortTtlKey, tokenResolver: shortTtlResolver });
216+
await shortTtlResolver;
217+
218+
const cachedEntry = cache.get(shortTtlKey);
219+
expect(cachedEntry).toMatchObject(shortTtlKey);
220+
221+
// Advance both the timer and the mocked current time
222+
const advanceBy = 31 * 1000;
223+
vi.advanceTimersByTime(advanceBy);
224+
dateNowSpy.mockImplementation(() => initialTime + advanceBy);
225+
226+
const cachedEntry2 = cache.get(shortTtlKey);
227+
expect(cachedEntry2).toBeUndefined();
228+
});
229+
230+
it('handles tokens with TTL greater than 60 seconds correctly', async () => {
231+
const cache = SessionTokenCache;
232+
233+
// Mock Date.now to return a fixed timestamp initially
234+
const initialTime = 1675876730000; // Same as our JWT's iat in milliseconds
235+
dateNowSpy = vi.spyOn(Date, 'now').mockImplementation(() => initialTime);
236+
237+
// Test with a 120-second TTL
238+
const longTtlJwt = createJwtWithTtl(120);
239+
const longTtlToken = new Token({
240+
object: 'token',
241+
id: 'long-ttl',
242+
jwt: longTtlJwt,
243+
});
244+
245+
const longTtlKey = { tokenId: 'long-ttl', audience: 'test' };
246+
const longTtlResolver = Promise.resolve(longTtlToken);
247+
cache.set({ ...longTtlKey, tokenResolver: longTtlResolver });
248+
await longTtlResolver;
249+
250+
// Check token is cached initially
251+
const cachedEntry = cache.get(longTtlKey);
252+
expect(cachedEntry).toMatchObject(longTtlKey);
253+
254+
// Advance 90 seconds - token should still be cached
255+
const firstAdvance = 90 * 1000;
256+
vi.advanceTimersByTime(firstAdvance);
257+
dateNowSpy.mockImplementation(() => initialTime + firstAdvance);
258+
259+
const cachedEntryAfter90s = cache.get(longTtlKey);
260+
expect(cachedEntryAfter90s).toMatchObject(longTtlKey);
261+
262+
// Advance to 121 seconds - token should be removed
263+
const secondAdvance = 31 * 1000;
264+
vi.advanceTimersByTime(secondAdvance);
265+
dateNowSpy.mockImplementation(() => initialTime + firstAdvance + secondAdvance);
266+
267+
const cachedEntryAfter121s = cache.get(longTtlKey);
268+
expect(cachedEntryAfter121s).toBeUndefined();
269+
});
270+
});
166271
});

0 commit comments

Comments
 (0)