Skip to content

Commit d976a82

Browse files
authored
feat(clerk-js): Send force_origin on skipCache token requests (#8106)
1 parent 25a73fb commit d976a82

5 files changed

Lines changed: 161 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Send `force_origin=true` body param on `/tokens` requests when `skipCache` is true, so FAPI Proxy routes to origin instead of Session Minter.

integration/tests/resiliency.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,8 +558,47 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('resilienc
558558
const lastBody = new URLSearchParams(tokenRequestBodies[tokenRequestBodies.length - 1]);
559559
expect(lastBody.has('token')).toBe(sessionMinterEnabled);
560560

561+
// skipCache: true should send force_origin=true in the POST body when sessionMinter is enabled.
562+
// Session.ts sets forceOrigin: 'true' which fapiClient serializes to force_origin=true
563+
expect(lastBody.has('force_origin')).toBe(sessionMinterEnabled);
564+
561565
// User should still be signed in after refresh
562566
await u.po.expect.toBeSignedIn();
563567
});
568+
569+
test('token refresh without skipCache does not send force_origin', async ({ page, context }) => {
570+
const u = createTestUtils({ app, page, context });
571+
572+
// Sign in
573+
await u.po.signIn.goTo();
574+
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
575+
await u.po.expect.toBeSignedIn();
576+
577+
// Track token request bodies
578+
const tokenRequestBodies: string[] = [];
579+
await context.route('**/v1/client/sessions/*/tokens*', async route => {
580+
const postData = route.request().postData();
581+
if (postData) {
582+
tokenRequestBodies.push(postData);
583+
}
584+
await route.continue();
585+
});
586+
587+
// Force a fresh token fetch without skipCache
588+
const token = await page.evaluate(async () => {
589+
const clerk = (window as any).Clerk;
590+
await clerk.session?.clearCache();
591+
return await clerk.session?.getToken();
592+
});
593+
594+
expect(token).toBeTruthy();
595+
596+
// Without skipCache, force_origin should NOT be present in the POST body
597+
expect(tokenRequestBodies.length).toBeGreaterThanOrEqual(1);
598+
const lastBody = new URLSearchParams(tokenRequestBodies[tokenRequestBodies.length - 1]);
599+
expect(lastBody.has('force_origin')).toBe(false);
600+
601+
await u.po.expect.toBeSignedIn();
602+
});
564603
});
565604
});

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ export class Session extends BaseResource implements SessionResource {
489489
: {
490490
organizationId: organizationId ?? null,
491491
...(sessionMinterEnabled && this.lastActiveToken ? { token: this.lastActiveToken.getRawString() } : {}),
492+
...(sessionMinterEnabled && skipCache ? { forceOrigin: 'true' } : {}),
492493
};
493494

494495
if (sessionMinterEnabled) {

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,108 @@ describe('Session', () => {
16981698
});
16991699
});
17001700

1701+
describe('sends force_origin in /tokens request body when skipCache is true', () => {
1702+
let dispatchSpy: ReturnType<typeof vi.spyOn>;
1703+
let fetchSpy: ReturnType<typeof vi.spyOn>;
1704+
1705+
beforeEach(() => {
1706+
dispatchSpy = vi.spyOn(eventBus, 'emit');
1707+
fetchSpy = vi.spyOn(BaseResource, '_fetch' as any);
1708+
BaseResource.clerk = clerkMock({
1709+
__internal_environment: {
1710+
authConfig: { sessionMinter: true },
1711+
},
1712+
}) as any;
1713+
});
1714+
1715+
afterEach(() => {
1716+
dispatchSpy?.mockRestore();
1717+
fetchSpy?.mockRestore();
1718+
BaseResource.clerk = null as any;
1719+
});
1720+
1721+
it('includes forceOrigin in body when skipCache is true', async () => {
1722+
const session = new Session({
1723+
status: 'active',
1724+
id: 'session_1',
1725+
object: 'session',
1726+
user: createUser({}),
1727+
last_active_organization_id: null,
1728+
last_active_token: { object: 'token', jwt: mockJwt },
1729+
actor: null,
1730+
created_at: new Date().getTime(),
1731+
updated_at: new Date().getTime(),
1732+
} as SessionJSON);
1733+
1734+
SessionTokenCache.clear();
1735+
1736+
fetchSpy.mockResolvedValueOnce({ object: 'token', jwt: mockJwt });
1737+
1738+
await session.getToken({ skipCache: true });
1739+
1740+
expect(fetchSpy).toHaveBeenCalledTimes(1);
1741+
expect(fetchSpy.mock.calls[0][0]).toMatchObject({
1742+
path: '/client/sessions/session_1/tokens',
1743+
method: 'POST',
1744+
body: expect.objectContaining({ forceOrigin: 'true' }),
1745+
search: { debug: 'skip_cache' },
1746+
});
1747+
expect(fetchSpy.mock.calls[0][0].body).not.toHaveProperty('debug');
1748+
});
1749+
1750+
it('does not include forceOrigin in body when skipCache is false or undefined', async () => {
1751+
const session = new Session({
1752+
status: 'active',
1753+
id: 'session_1',
1754+
object: 'session',
1755+
user: createUser({}),
1756+
last_active_organization_id: null,
1757+
last_active_token: { object: 'token', jwt: mockJwt },
1758+
actor: null,
1759+
created_at: new Date().getTime(),
1760+
updated_at: new Date().getTime(),
1761+
} as SessionJSON);
1762+
1763+
SessionTokenCache.clear();
1764+
1765+
fetchSpy.mockResolvedValueOnce({ object: 'token', jwt: mockJwt });
1766+
1767+
await session.getToken();
1768+
1769+
expect(fetchSpy).toHaveBeenCalledTimes(1);
1770+
expect(fetchSpy.mock.calls[0][0].body).not.toHaveProperty('forceOrigin');
1771+
});
1772+
1773+
it('does not include forceOrigin when sessionMinter is false even with skipCache true', async () => {
1774+
BaseResource.clerk = clerkMock({
1775+
__internal_environment: {
1776+
authConfig: { sessionMinter: false },
1777+
},
1778+
}) as any;
1779+
1780+
const session = new Session({
1781+
status: 'active',
1782+
id: 'session_1',
1783+
object: 'session',
1784+
user: createUser({}),
1785+
last_active_organization_id: null,
1786+
last_active_token: { object: 'token', jwt: mockJwt },
1787+
actor: null,
1788+
created_at: new Date().getTime(),
1789+
updated_at: new Date().getTime(),
1790+
} as SessionJSON);
1791+
1792+
SessionTokenCache.clear();
1793+
1794+
fetchSpy.mockResolvedValueOnce({ object: 'token', jwt: mockJwt });
1795+
1796+
await session.getToken({ skipCache: true });
1797+
1798+
expect(fetchSpy).toHaveBeenCalledTimes(1);
1799+
expect(fetchSpy.mock.calls[0][0].body).not.toHaveProperty('forceOrigin');
1800+
});
1801+
});
1802+
17011803
describe('origin outage mode fallback', () => {
17021804
let dispatchSpy: ReturnType<typeof vi.spyOn>;
17031805
let fetchSpy: ReturnType<typeof vi.spyOn>;

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,20 @@ describe('Token', () => {
152152
const [url] = (global.fetch as Mock).mock.calls[0];
153153
expect(url.toString()).not.toContain('debug=skip_cache');
154154
});
155+
156+
it('includes force_origin=true in POST body when provided', async () => {
157+
mockFetch(true, 200, {
158+
object: 'token',
159+
jwt: mockJwt,
160+
});
161+
BaseResource.clerk = { getFapiClient: () => createFapiClient(baseFapiClientOptions) } as any;
162+
163+
await Token.create('/path/to/tokens', { forceOrigin: 'true' });
164+
165+
const [url, options] = (global.fetch as Mock).mock.calls[0];
166+
expect(options.body).toContain('force_origin=true');
167+
expect(url.toString()).not.toContain('force_origin');
168+
});
155169
});
156170

157171
describe('create with search parameters', () => {

0 commit comments

Comments
 (0)