Skip to content

Commit a3082a3

Browse files
committed
fix(clerk-js): keep lastActiveToken monotonic on an empty cache slot
#applyIncomingLastActiveToken only used the active-org cache slot as the freshness baseline, so when that slot was empty (evicted, or a token fetch still pending leaves resolvedToken null) a strictly-staler or wrong-org last_active_token from fromJSON was adopted, regressing lastActiveToken. Fall back to the token already held when the cache slot is empty, and reject a wrong-org incoming token even when the slot is empty.
1 parent 98d0fcc commit a3082a3

2 files changed

Lines changed: 55 additions & 6 deletions

File tree

‎packages/clerk-js/src/core/resources/Session.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,19 +220,22 @@ export class Session extends BaseResource implements SessionResource {
220220
const current = SessionTokenCache.get({ tokenId })?.entry.resolvedToken ?? null;
221221

222222
// A token whose explicit org claim differs from the active org is stale-context: it must
223-
// not win lastActiveToken over a cached active-context token, nor be written into the
224-
// active cache slot that getToken() reads. A token with no org claim is legacy/personal
225-
// and counts as same-context.
223+
// not win lastActiveToken nor be written into the active cache slot that getToken() reads.
224+
// A token with no org claim is legacy/personal and counts as same-context. The freshness
225+
// baseline falls back to the token we already hold when the active cache slot is empty
226+
// (evicted, or a fetch still pending leaves resolvedToken null), so a staler or wrong-org
227+
// piggyback can never regress lastActiveToken.
226228
const incomingOrg = tokenOrgId(incoming);
227229
const sameContext = !incomingOrg || normalizeOrgId(incomingOrg) === normalizeOrgId(this.lastActiveOrganizationId);
230+
const baseline = current ?? this.lastActiveToken;
228231

229-
if (current && (!sameContext || isStrictlyStalerJwt(incoming, current))) {
230-
this.lastActiveToken = current;
232+
if (!sameContext || (baseline && isStrictlyStalerJwt(incoming, baseline))) {
233+
this.lastActiveToken = baseline;
231234
return;
232235
}
233236

234237
this.lastActiveToken = incoming;
235-
if (sameContext && (!current || current.getRawString() !== incoming.getRawString())) {
238+
if (!current || current.getRawString() !== incoming.getRawString()) {
236239
SessionTokenCache.set({
237240
tokenId,
238241
tokenResolver: Promise.resolve(incoming),

‎packages/clerk-js/src/core/resources/__tests__/Session.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,6 +2419,52 @@ describe('Session', () => {
24192419
expect(session.lastActiveToken?.getRawString()).toBe(legacy);
24202420
expect(SessionTokenCache.get({ tokenId })?.entry.resolvedToken?.getRawString()).toBe(legacy);
24212421
});
2422+
2423+
it('keeps the held lastActiveToken when a strictly-staler same-context token arrives and the cache slot is empty', async () => {
2424+
const high = createJwtWithOiat(NOW, NOW + 30);
2425+
const low = createJwtWithOiat(NOW, NOW);
2426+
const session = makeSession();
2427+
2428+
(session as any).fromJSON(sessionJsonWith(high));
2429+
await flush();
2430+
expect(session.lastActiveToken?.getRawString()).toBe(high);
2431+
2432+
SessionTokenCache.clear();
2433+
2434+
(session as any).fromJSON(sessionJsonWith(low));
2435+
await flush();
2436+
2437+
expect(session.lastActiveToken?.getRawString()).toBe(high);
2438+
});
2439+
2440+
it('rejects a wrong-org last_active_token even when the active-org cache slot is empty', async () => {
2441+
const orgBToken = createJwtWithOiat(NOW, NOW + 30, { org: 'org_B' });
2442+
const orgAToken = createJwtWithOiat(NOW, NOW + 60, { org: 'org_A' });
2443+
const session = makeSession({ last_active_organization_id: 'org_B' } as Partial<SessionJSON>);
2444+
const orgBJson = (jwt: string) =>
2445+
({
2446+
status: 'active',
2447+
id: 'session_1',
2448+
object: 'session',
2449+
user: createUser({}),
2450+
last_active_organization_id: 'org_B',
2451+
actor: null,
2452+
created_at: Date.now(),
2453+
updated_at: Date.now(),
2454+
last_active_token: { object: 'token', jwt },
2455+
}) as SessionJSON;
2456+
2457+
(session as any).fromJSON(orgBJson(orgBToken));
2458+
await flush();
2459+
expect(session.lastActiveToken?.getRawString()).toBe(orgBToken);
2460+
2461+
SessionTokenCache.clear();
2462+
2463+
(session as any).fromJSON(orgBJson(orgAToken));
2464+
await flush();
2465+
2466+
expect(session.lastActiveToken?.getRawString()).toBe(orgBToken);
2467+
});
24222468
});
24232469
});
24242470
});

0 commit comments

Comments
 (0)