diff --git a/.changeset/early-ads-notice.md b/.changeset/early-ads-notice.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/early-ads-notice.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 0c3c23ee0d..1600c8d764 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -118,7 +118,9 @@ export class Session extends BaseResource implements SessionResource { // e.g. session id is 'sess_abc12345' and jwt template name is 'haris' // The session token ID will be 'sess_abc12345' and the jwt template token ID will be 'sess_abc12345-haris' #getCacheId(template?: string, organizationId?: string) { - return [this.id, template, organizationId, this.updatedAt.getTime()].filter(Boolean).join('-'); + const resolvedOrganizationId = + typeof organizationId === 'undefined' ? this.lastActiveOrganizationId : organizationId; + return [this.id, template, resolvedOrganizationId, this.updatedAt.getTime()].filter(Boolean).join('-'); } protected fromJSON(data: SessionJSON | null): this { @@ -151,12 +153,12 @@ export class Session extends BaseResource implements SessionResource { return null; } - const { - leewayInSeconds, - template, - skipCache = false, - organizationId = Session.clerk.organization?.id, - } = options || {}; + const { leewayInSeconds, template, skipCache = false } = options || {}; + + // If no organization ID is provided, default to the selected organization in memory + // Note: this explicitly allows passing `null` or `""`, which should select the personal workspace. + const organizationId = + typeof options?.organizationId === 'undefined' ? Session.clerk.organization?.id : options?.organizationId; if (!template && Number(leewayInSeconds) >= 60) { throw new Error('Leeway can not exceed the token lifespan (60 seconds)'); @@ -166,7 +168,7 @@ export class Session extends BaseResource implements SessionResource { const cachedEntry = skipCache ? undefined : SessionTokenCache.get({ tokenId }, leewayInSeconds); // Dispatch tokenUpdate only for __session tokens with the session's active organization ID, and not JWT templates - const shouldDispatchTokenUpdate = !template && options?.organizationId === Session.clerk.organization?.id; + const shouldDispatchTokenUpdate = !template && organizationId === Session.clerk.organization?.id; if (cachedEntry) { const cachedToken = await cachedEntry.tokenResolver; @@ -178,7 +180,7 @@ export class Session extends BaseResource implements SessionResource { } const path = template ? `${this.path()}/tokens/${template}` : `${this.path()}/tokens`; // TODO: update template endpoint to accept organizationId - const params = template ? {} : { ...(organizationId && { organizationId }) }; + const params = template ? {} : { organizationId }; const tokenResolver = Token.create(path, params); SessionTokenCache.set({ tokenId, tokenResolver }); return tokenResolver.then(token => { diff --git a/packages/clerk-js/src/core/resources/__tests__/Session.test.ts b/packages/clerk-js/src/core/resources/__tests__/Session.test.ts index 6ac27eda98..03d56d2c11 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Session.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Session.test.ts @@ -3,9 +3,14 @@ import type { OrganizationJSON, SessionJSON } from '@clerk/types'; import { eventBus } from '../../events'; import { createFapiClient } from '../../fapiClient'; import { clerkMock, createUser, mockDevClerkInstance, mockJwt, mockNetworkFailedFetch } from '../../test/fixtures'; +import { SessionTokenCache } from '../../tokenCache'; import { BaseResource, Organization, Session } from '../internal'; describe('Session', () => { + afterEach(() => { + SessionTokenCache.clear(); + }); + describe('creating new session', () => { let dispatchSpy; @@ -19,6 +24,7 @@ describe('Session', () => { BaseResource.clerk = null as any; // @ts-ignore global.fetch?.mockClear(); + SessionTokenCache.clear(); }); it('dispatches token:update event on initialization with lastActiveToken', () => { @@ -53,7 +59,56 @@ describe('Session', () => { BaseResource.clerk = null as any; }); - it('dispatches token:update event on getToken', async () => { + it('dispatches token:update event on getToken without active organization', async () => { + const session = new Session({ + status: 'active', + id: 'session_1', + object: 'session', + user: createUser({}), + last_active_organization_id: null, + actor: null, + created_at: new Date().getTime(), + updated_at: new Date().getTime(), + } as SessionJSON); + + await session.getToken(); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy.mock.calls[0]).toMatchSnapshot(); + }); + + it('hydrates token cache from lastActiveToken', async () => { + BaseResource.clerk = clerkMock({ + organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON), + }) as any; + + const session = new Session({ + status: 'active', + id: 'session_1', + object: 'session', + user: createUser({}), + last_active_organization_id: 'activeOrganization', + last_active_token: { object: 'token', jwt: mockJwt }, + actor: null, + created_at: new Date().getTime(), + updated_at: new Date().getTime(), + } as SessionJSON); + + const token = await session.getToken(); + + await session.getToken({ organizationId: 'activeOrganization' }); + + expect(BaseResource.clerk.getFapiClient().request).not.toHaveBeenCalled(); + + expect(token).toEqual(mockJwt); + expect(dispatchSpy).toHaveBeenCalledTimes(3); + }); + + it('dispatches token:update event on getToken with active organization', async () => { + BaseResource.clerk = clerkMock({ + organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON), + }) as any; + const session = new Session({ status: 'active', id: 'session_1', @@ -72,6 +127,10 @@ describe('Session', () => { }); it('does not dispatch token:update if template is provided', async () => { + BaseResource.clerk = clerkMock({ + organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON), + }) as any; + const session = new Session({ status: 'active', id: 'session_1', @@ -92,6 +151,7 @@ describe('Session', () => { BaseResource.clerk = clerkMock({ organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON), }) as any; + const session = new Session({ status: 'active', id: 'session_1', @@ -112,6 +172,7 @@ describe('Session', () => { BaseResource.clerk = clerkMock({ organization: new Organization({ id: 'anotherOrganization' } as OrganizationJSON), }) as any; + const session = new Session({ status: 'active', id: 'session_1', @@ -163,6 +224,7 @@ describe('Session', () => { const token = await session.getToken(); + expect(global.fetch).toHaveBeenCalled(); expect(dispatchSpy).toHaveBeenCalledTimes(1); expect(token).toEqual(null); }); diff --git a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Session.test.ts.snap b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Session.test.ts.snap index be99433295..0b53b387d0 100644 --- a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Session.test.ts.snap +++ b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Session.test.ts.snap @@ -34,7 +34,41 @@ exports[`Session creating new session dispatches token:update event on initializ ] `; -exports[`Session getToken() dispatches token:update event on getToken 1`] = ` +exports[`Session getToken() dispatches token:update event on getToken with active organization 1`] = ` +[ + "token:update", + { + "token": Token { + "getRawString": [Function], + "jwt": { + "claims": { + "__raw": "eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.n1Usc-DLDftqA0Xb-_2w8IGs4yjCmwc5RngwbSRvwevuZOIuRoeHmE2sgCdEvjfJEa7ewL6EVGVcM557TWPW--g_J1XQPwBy8tXfz7-S73CEuyRFiR97L2AHRdvRtvGtwR-o6l8aHaFxtlmfWbQXfg4kFJz2UGe9afmh3U9-f_4JOZ5fa3mI98UMy1-bo20vjXeWQ9aGrqaxHQxjnzzC-1Kpi5LdPvhQ16H0dPB8MHRTSM5TAuLKTpPV7wqixmbtcc2-0k6b9FKYZNqRVTaIyV-lifZloBvdzlfOF8nW1VVH_fx-iW5Q3hovHFcJIULHEC1kcAYTubbxzpgeVQepGg", + "azp": "https://accounts.inspired.puma-74.lcl.dev", + "exp": 1666648310, + "iat": 1666648250, + "iss": "https://clerk.inspired.puma-74.lcl.dev", + "nbf": 1666648240, + "sid": "sess_2GbDB4enNdCa5vS1zpC3Xzg9tK9", + "sub": "user_2GIpXOEpVyJw51rkZn9Kmnc6Sxr", + }, + "encoded": { + "header": "eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ", + "payload": "eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9", + "signature": "n1Usc-DLDftqA0Xb-_2w8IGs4yjCmwc5RngwbSRvwevuZOIuRoeHmE2sgCdEvjfJEa7ewL6EVGVcM557TWPW--g_J1XQPwBy8tXfz7-S73CEuyRFiR97L2AHRdvRtvGtwR-o6l8aHaFxtlmfWbQXfg4kFJz2UGe9afmh3U9-f_4JOZ5fa3mI98UMy1-bo20vjXeWQ9aGrqaxHQxjnzzC-1Kpi5LdPvhQ16H0dPB8MHRTSM5TAuLKTpPV7wqixmbtcc2-0k6b9FKYZNqRVTaIyV-lifZloBvdzlfOF8nW1VVH_fx-iW5Q3hovHFcJIULHEC1kcAYTubbxzpgeVQepGg", + }, + "header": { + "alg": "RS256", + "kid": "ins_2GIoQhbUpy0hX7B2cVkuTMinXoD", + "typ": "JWT", + }, + }, + "pathRoot": "/client/sessions/session_1/tokens", + }, + }, +] +`; + +exports[`Session getToken() dispatches token:update event on getToken without active organization 1`] = ` [ "token:update", {