Skip to content

Commit 5bb8efc

Browse files
authored
fix(js): keep the Device Intelligence token per instance on a shared origin (#10536)
1 parent dd24917 commit 5bb8efc

7 files changed

Lines changed: 167 additions & 60 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+
Fix sign-ins and sign-ups being blocked by Device Intelligence checks when the same origin served more than one Clerk instance, such as an app moving from development to production keys. The Device Intelligence token is now stored per instance, so a token acquired for one instance is no longer sent to another.

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Protect } from '../protect';
55
import { __internal_resetProtectStorage } from '../protectSession';
66
import type { Environment } from '../resources';
77

8+
const FRONTEND_API = 'clerk.example.com';
9+
810
const environment = (loaders: unknown[]): Environment => ({ protectConfig: { loaders } }) as unknown as Environment;
911

1012
/**
@@ -61,7 +63,7 @@ afterEach(() => {
6163

6264
describe('Protect.load', () => {
6365
it('does nothing without a protect config', () => {
64-
new Protect().load(environment([]));
66+
new Protect().load(environment([]), FRONTEND_API);
6567
expect(document.head.querySelector('script')).toBeNull();
6668
expect(localStorage.getItem('__clerk_protect_pid')).toBeNull();
6769
});
@@ -72,6 +74,7 @@ describe('Protect.load', () => {
7274
environment([
7375
{ target: 'head', type: 'script', attributes: { 'data-loader': 'https://loader.example.com/ins_2abc.js' } },
7476
]),
77+
FRONTEND_API,
7578
);
7679

7780
expect(document.head.querySelector('script')?.getAttribute('data-loader')).toBe(
@@ -96,6 +99,7 @@ describe('Protect.load', () => {
9699
},
97100
}),
98101
]),
102+
FRONTEND_API,
99103
);
100104

101105
const element = await injected('script');
@@ -111,7 +115,7 @@ describe('Protect.load', () => {
111115

112116
it('substitutes placeholders in textContent as well as attributes', async () => {
113117
const protect = new Protect();
114-
protect.load(environment([loader({ text_content: 'window.__vendor_cid = "{cid}";' })]));
118+
protect.load(environment([loader({ text_content: 'window.__vendor_cid = "{cid}";' })]), FRONTEND_API);
115119

116120
const element = await injected('script');
117121
expect(element.textContent).toBe(`window.__vendor_cid = "${element.getAttribute('data-cid')}";`);
@@ -120,14 +124,14 @@ describe('Protect.load', () => {
120124

121125
it('never interpolates {instance_id}', async () => {
122126
const protect = new Protect();
123-
protect.load(environment([loader({ attributes: { 'data-src': '{instance_id}/{cid}.js' } })]));
127+
protect.load(environment([loader({ attributes: { 'data-src': '{instance_id}/{cid}.js' } })]), FRONTEND_API);
124128

125129
expect((await injected('script')).getAttribute('data-src')).toContain('{instance_id}');
126130
});
127131

128132
it('attaches the token once it has been acquired', async () => {
129133
const protect = new Protect();
130-
protect.load(environment([loader()]));
134+
protect.load(environment([loader()]), FRONTEND_API);
131135

132136
serveInline(await injected('script'));
133137

@@ -139,7 +143,7 @@ describe('Protect.load', () => {
139143

140144
it('still applies the other loaders when a token for this browser session is shared', async () => {
141145
localStorage.setItem(
142-
'__clerk_protect_st',
146+
`__clerk_protect_st:${FRONTEND_API}`,
143147
JSON.stringify({ token: 'v1.cached.mac', exp: nowSeconds() + 43_200, rid: 'b'.repeat(26), at: Date.now() }),
144148
);
145149

@@ -149,6 +153,7 @@ describe('Protect.load', () => {
149153
loader({ attributes: { 'data-role': 'detection' } }),
150154
loader({ attributes: { 'data-cid': '{cid}', 'data-role': 'token' } }),
151155
]),
156+
FRONTEND_API,
152157
);
153158

154159
// Acquisition happens once per browser session, so the token loader is skipped…
@@ -162,7 +167,7 @@ describe('Protect.load', () => {
162167
vi.spyOn(Math, 'random').mockReturnValue(0.9);
163168

164169
const protect = new Protect();
165-
protect.load(environment([loader({ rollout: 0.1 })]));
170+
protect.load(environment([loader({ rollout: 0.1 })]), FRONTEND_API);
166171

167172
expect(document.head.querySelector('script')).toBeNull();
168173
// Out of rollout means Protect is off for this browser, so there is nothing to report either.
@@ -171,7 +176,7 @@ describe('Protect.load', () => {
171176

172177
it('reports script_error when the loader element fails to load', async () => {
173178
const protect = new Protect();
174-
protect.load(environment([loader({ token_timeout_ms: 5_000 })]));
179+
protect.load(environment([loader({ token_timeout_ms: 5_000 })]), FRONTEND_API);
175180

176181
(await injected('script')).dispatchEvent(new Event('error'));
177182

@@ -182,13 +187,15 @@ describe('Protect.load', () => {
182187
const protect = new Protect();
183188

184189
// The config is server-controlled and cached; a bad entry must not take Clerk.load() down.
185-
expect(() => protect.load(environment([null, loader({ attributes: { 'data-role': 'good' } })]))).not.toThrow();
190+
expect(() =>
191+
protect.load(environment([null, loader({ attributes: { 'data-role': 'good' } })]), FRONTEND_API),
192+
).not.toThrow();
186193

187194
expect(document.head.querySelector('[data-role="good"]')).not.toBeNull();
188195
});
189196

190197
it('survives a loader config that is not an array of objects at all', () => {
191198
const protect = new Protect();
192-
expect(() => protect.load(environment(['nope', 42, undefined]))).not.toThrow();
199+
expect(() => protect.load(environment(['nope', 42, undefined]), FRONTEND_API)).not.toThrow();
193200
});
194201
});

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Protect } from '../protect';
55
import { __internal_resetProtectStorage } from '../protectSession';
66
import type { Environment } from '../resources';
77

8+
const FRONTEND_API = 'clerk.example.com';
9+
810
const environment = (loaders: unknown[]): Environment => ({ protectConfig: { loaders } }) as unknown as Environment;
911

1012
/** No `src`, so jsdom does not fetch a real URL and race the assertions here. */
@@ -24,14 +26,14 @@ describe('Protect.challengeLoadTimeoutMs', () => {
2426

2527
it('is undefined when no loader asks for one, so the caller falls back', () => {
2628
const protect = new Protect();
27-
protect.load(environment([loader()]));
29+
protect.load(environment([loader()]), FRONTEND_API);
2830

2931
expect(protect.challengeLoadTimeoutMs).toBeUndefined();
3032
});
3133

3234
it('reports the value from the loader this browser was assigned', () => {
3335
const protect = new Protect();
34-
protect.load(environment([loader({ challenge_load_timeout_ms: 25_000 })]));
36+
protect.load(environment([loader({ challenge_load_timeout_ms: 25_000 })]), FRONTEND_API);
3537

3638
expect(protect.challengeLoadTimeoutMs).toBe(25_000);
3739
});
@@ -49,6 +51,7 @@ describe('Protect.challengeLoadTimeoutMs', () => {
4951
// The incumbent, which this browser does get.
5052
loader({ challenge_load_timeout_ms: 45_000 }),
5153
]),
54+
FRONTEND_API,
5255
);
5356

5457
expect(protect.challengeLoadTimeoutMs).toBe(45_000);
@@ -58,7 +61,7 @@ describe('Protect.challengeLoadTimeoutMs', () => {
5861
vi.spyOn(Math, 'random').mockReturnValue(0.01);
5962

6063
const protect = new Protect();
61-
protect.load(environment([loader({ rollout: 0.1, challenge_load_timeout_ms: 25_000 }), loader()]));
64+
protect.load(environment([loader({ rollout: 0.1, challenge_load_timeout_ms: 25_000 }), loader()]), FRONTEND_API);
6265

6366
expect(protect.challengeLoadTimeoutMs).toBe(25_000);
6467
});
@@ -71,6 +74,7 @@ describe('Protect.challengeLoadTimeoutMs', () => {
7174
loader({ challenge_load_timeout_ms: Number.POSITIVE_INFINITY }),
7275
loader({ challenge_load_timeout_ms: 'soon' as unknown as number }),
7376
]),
77+
FRONTEND_API,
7478
);
7579

7680
expect(protect.challengeLoadTimeoutMs).toBeUndefined();
@@ -84,6 +88,7 @@ describe('Protect.challengeLoadTimeoutMs', () => {
8488
loader({ challenge_load_timeout_ms: 25_000 }),
8589
loader({ challenge_load_timeout_ms: 45_000 }),
8690
]),
91+
FRONTEND_API,
8792
);
8893

8994
expect(protect.challengeLoadTimeoutMs).toBe(25_000);

0 commit comments

Comments
 (0)