Skip to content

Commit cb1a62f

Browse files
dmoernerclaude
andauthored
fix(clerk-js): Bypass captcha on OAuth backport for Core 2 (#8734)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 29388cd commit cb1a62f

3 files changed

Lines changed: 92 additions & 15 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 new OAuth users hanging on the SSO callback. When an OAuth sign-in transfers to a sign-up, the captcha is now correctly bypassed for providers in the captcha OAuth bypass list, instead of unconditionally showing a captcha that could never be completed.

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

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,6 @@ export class SignUp extends BaseResource implements SignUpResource {
182182
finalParams = { ...finalParams, ...captchaParams };
183183
}
184184

185-
if (finalParams.transfer && this.shouldBypassCaptchaForAttempt(finalParams)) {
186-
const strategy = SignUp.clerk.client?.signIn.firstFactorVerification.strategy;
187-
if (strategy) {
188-
finalParams = { ...finalParams, strategy: strategy as SignUpCreateParams['strategy'] };
189-
}
190-
}
191-
192185
return this._basePost({
193186
path: this.pathRoot,
194187
body: normalizeUnsafeMetadata(finalParams),
@@ -562,17 +555,13 @@ export class SignUp extends BaseResource implements SignUpResource {
562555
* We delegate bot detection to the following providers, instead of relying on turnstile exclusively
563556
*/
564557
protected shouldBypassCaptchaForAttempt(params: SignUpCreateParams) {
565-
if (!params.strategy) {
566-
return false;
567-
}
568-
569558
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
570559
const captchaOauthBypass = SignUp.clerk.__unstable__environment!.displayConfig.captchaOauthBypass;
571560

572-
if (captchaOauthBypass.some(strategy => strategy === params.strategy)) {
573-
return true;
574-
}
575-
561+
// OAuth transfers: If we delegate captcha detection to the OAuth provider,
562+
// do not show another captcha on sign up. The strategy lives on the sign in
563+
// verification rather than on `params`, so this must be checked before any
564+
// early return based on `params.strategy`.
576565
if (
577566
params.transfer &&
578567
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -581,6 +570,12 @@ export class SignUp extends BaseResource implements SignUpResource {
581570
return true;
582571
}
583572

573+
// OAuth sign ups: If we delegate captcha detection to the OAuth provider,
574+
// do not show another captcha on sign up.
575+
if (params.strategy && captchaOauthBypass.some(strategy => strategy === params.strategy)) {
576+
return true;
577+
}
578+
584579
return false;
585580
}
586581

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,90 @@ vi.mock('../../../utils/captcha/CaptchaChallenge', () => ({
2525
})),
2626
}));
2727

28+
// Import the mocked CaptchaChallenge after mocking
29+
import { CaptchaChallenge } from '../../../utils/captcha/CaptchaChallenge';
30+
2831
describe('SignUp', () => {
2932
it('can be serialized with JSON.stringify', () => {
3033
const signUp = new SignUp();
3134
const snapshot = JSON.stringify(signUp);
3235
expect(snapshot).toBeDefined();
3336
});
3437

38+
describe('create', () => {
39+
beforeEach(() => {
40+
SignUp.clerk = {
41+
__unstable__environment: {
42+
displayConfig: {
43+
captchaOauthBypass: [],
44+
},
45+
},
46+
} as any;
47+
});
48+
49+
afterEach(() => {
50+
vi.clearAllMocks();
51+
vi.unstubAllGlobals();
52+
SignUp.clerk = {} as any;
53+
});
54+
55+
it('skips captcha challenge for oauth transfer when strategy is in captchaOauthBypass', async () => {
56+
const mockFetch = vi.fn().mockResolvedValue({
57+
client: null,
58+
response: { id: 'signup_123', status: 'missing_requirements' },
59+
});
60+
BaseResource._fetch = mockFetch;
61+
SignUp.clerk = {
62+
client: {
63+
signIn: {
64+
firstFactorVerification: {
65+
status: 'transferable',
66+
strategy: 'oauth_google',
67+
},
68+
},
69+
},
70+
__unstable__environment: {
71+
displayConfig: {
72+
captchaOauthBypass: ['oauth_google', 'oauth_apple'],
73+
},
74+
},
75+
} as any;
76+
77+
const signUp = new SignUp();
78+
await signUp.create({ transfer: true });
79+
80+
expect(CaptchaChallenge).not.toHaveBeenCalled();
81+
});
82+
83+
it('does not skip captcha challenge for enterprise_sso transfer', async () => {
84+
const mockFetch = vi.fn().mockResolvedValue({
85+
client: null,
86+
response: { id: 'signup_123', status: 'missing_requirements' },
87+
});
88+
BaseResource._fetch = mockFetch;
89+
SignUp.clerk = {
90+
client: {
91+
signIn: {
92+
firstFactorVerification: {
93+
status: 'transferable',
94+
strategy: 'enterprise_sso',
95+
},
96+
},
97+
},
98+
__unstable__environment: {
99+
displayConfig: {
100+
captchaOauthBypass: [],
101+
},
102+
},
103+
} as any;
104+
105+
const signUp = new SignUp();
106+
await signUp.create({ transfer: true });
107+
108+
expect(CaptchaChallenge).toHaveBeenCalledWith(SignUp.clerk);
109+
});
110+
});
111+
35112
describe('SignUpFuture', () => {
36113
it('can be serialized with JSON.stringify', () => {
37114
const signUp = new SignUp();

0 commit comments

Comments
 (0)