Skip to content

Commit 875aee5

Browse files
dstaleyjacekradko
andauthored
fix(clerk-js): Conditionally call captcha during sign-up (#7835)
Co-authored-by: Jacek Radko <jacek@clerk.dev>
1 parent 7772f45 commit 875aee5

3 files changed

Lines changed: 138 additions & 3 deletions

File tree

.changeset/sour-walls-exist.md

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+
Fixes issue where captcha was always called during signup.

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -786,11 +786,42 @@ class SignUpFuture implements SignUpFutureResource {
786786
return this.#canBeDiscarded;
787787
}
788788

789-
private async getCaptchaToken(): Promise<{
789+
private shouldBypassCaptchaForAttempt(params: { strategy?: string; transfer?: boolean }) {
790+
if (!params.strategy) {
791+
return false;
792+
}
793+
794+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
795+
const captchaOauthBypass = SignUp.clerk.__internal_environment!.displayConfig.captchaOauthBypass;
796+
797+
if (captchaOauthBypass.some(strategy => strategy === params.strategy)) {
798+
return true;
799+
}
800+
801+
if (
802+
params.transfer &&
803+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
804+
captchaOauthBypass.some(strategy => strategy === SignUp.clerk.client!.signIn.firstFactorVerification.strategy)
805+
) {
806+
return true;
807+
}
808+
809+
return false;
810+
}
811+
812+
private async getCaptchaToken(params: { strategy?: string; transfer?: boolean } = {}): Promise<{
790813
captchaToken?: string;
791814
captchaWidgetType?: CaptchaWidgetType;
792815
captchaError?: unknown;
793816
}> {
817+
if (__BUILD_DISABLE_RHC__ || SignUp.clerk.client?.captchaBypass || this.shouldBypassCaptchaForAttempt(params)) {
818+
return {
819+
captchaToken: undefined,
820+
captchaWidgetType: undefined,
821+
captchaError: undefined,
822+
};
823+
}
824+
794825
const captchaChallenge = new CaptchaChallenge(SignUp.clerk);
795826
const response = await captchaChallenge.managedOrInvisible({ action: 'signup' });
796827
if (!response) {
@@ -802,7 +833,7 @@ class SignUpFuture implements SignUpFutureResource {
802833
}
803834

804835
private async _create(params: SignUpFutureCreateParams): Promise<void> {
805-
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken();
836+
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(params);
806837

807838
const body: Record<string, unknown> = {
808839
transfer: params.transfer,
@@ -955,7 +986,7 @@ class SignUpFuture implements SignUpFutureResource {
955986
popup,
956987
} = params;
957988
return runAsyncResourceTask(this.#resource, async () => {
958-
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken();
989+
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken({ strategy });
959990

960991
let redirectUrlComplete = redirectUrl;
961992
try {

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ vi.mock('../../../utils/authenticateWithPopup', async () => {
1616

1717
// Import the mocked function after mocking
1818
import { _futureAuthenticateWithPopup } from '../../../utils/authenticateWithPopup';
19+
import { CaptchaChallenge } from '../../../utils/captcha/CaptchaChallenge';
1920

2021
// Mock the CaptchaChallenge module
2122
vi.mock('../../../utils/captcha/CaptchaChallenge', () => ({
@@ -42,9 +43,14 @@ describe('SignUp', () => {
4243
});
4344

4445
describe('create', () => {
46+
beforeEach(() => {
47+
SignUp.clerk = {} as any;
48+
});
49+
4550
afterEach(() => {
4651
vi.clearAllMocks();
4752
vi.unstubAllGlobals();
53+
SignUp.clerk = {} as any;
4854
});
4955

5056
it('includes locale in request when navigator.language is available', async () => {
@@ -106,6 +112,46 @@ describe('SignUp', () => {
106112

107113
expect(result).toHaveProperty('error', null);
108114
});
115+
116+
it('runs captcha challenge when bypass is not enabled', async () => {
117+
const mockFetch = vi.fn().mockResolvedValue({
118+
client: null,
119+
response: { id: 'signup_123', status: 'missing_requirements' },
120+
});
121+
BaseResource._fetch = mockFetch;
122+
123+
const signUp = new SignUp();
124+
await signUp.__internal_future.create({ emailAddress: 'user@example.com' });
125+
126+
expect(CaptchaChallenge).toHaveBeenCalledWith(SignUp.clerk);
127+
const challengeInstance = vi.mocked(CaptchaChallenge).mock.results[0]?.value as {
128+
managedOrInvisible: ReturnType<typeof vi.fn>;
129+
};
130+
expect(challengeInstance.managedOrInvisible).toHaveBeenCalledWith({ action: 'signup' });
131+
expect(mockFetch.mock.calls[0]?.[0].body).toHaveProperty('captchaToken', 'mock_token');
132+
expect(mockFetch.mock.calls[0]?.[0].body).toHaveProperty('captchaWidgetType', 'invisible');
133+
});
134+
135+
it('skips captcha challenge when client captcha bypass is enabled', async () => {
136+
const mockFetch = vi.fn().mockResolvedValue({
137+
client: null,
138+
response: { id: 'signup_123', status: 'missing_requirements' },
139+
});
140+
BaseResource._fetch = mockFetch;
141+
SignUp.clerk = {
142+
client: {
143+
captchaBypass: true,
144+
},
145+
} as any;
146+
147+
const signUp = new SignUp();
148+
await signUp.__internal_future.create({ emailAddress: 'user@example.com' });
149+
150+
expect(CaptchaChallenge).not.toHaveBeenCalled();
151+
expect(mockFetch.mock.calls[0]?.[0].body).toHaveProperty('captchaToken', undefined);
152+
expect(mockFetch.mock.calls[0]?.[0].body).toHaveProperty('captchaWidgetType', undefined);
153+
expect(mockFetch.mock.calls[0]?.[0].body).toHaveProperty('captchaError', undefined);
154+
});
109155
});
110156

111157
describe('sendPhoneCode', () => {
@@ -333,6 +379,7 @@ describe('SignUp', () => {
333379
afterEach(() => {
334380
vi.clearAllMocks();
335381
vi.unstubAllGlobals();
382+
SignUp.clerk = {} as any;
336383
});
337384

338385
it('handles relative redirectUrl by converting to absolute', async () => {
@@ -341,6 +388,11 @@ describe('SignUp', () => {
341388
const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
342389
SignUp.clerk = {
343390
buildUrlWithAuth: mockBuildUrlWithAuth,
391+
__internal_environment: {
392+
displayConfig: {
393+
captchaOauthBypass: [],
394+
},
395+
},
344396
} as any;
345397

346398
const mockFetch = vi.fn().mockResolvedValue({
@@ -383,6 +435,11 @@ describe('SignUp', () => {
383435
const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
384436
SignUp.clerk = {
385437
buildUrlWithAuth: mockBuildUrlWithAuth,
438+
__internal_environment: {
439+
displayConfig: {
440+
captchaOauthBypass: [],
441+
},
442+
},
386443
} as any;
387444

388445
const mockFetch = vi.fn().mockResolvedValue({
@@ -436,6 +493,9 @@ describe('SignUp', () => {
436493
buildUrl: vi.fn().mockImplementation(path => 'https://example.com' + path),
437494
frontendApi: 'clerk.example.com',
438495
__internal_environment: {
496+
displayConfig: {
497+
captchaOauthBypass: [],
498+
},
439499
reload: vi.fn().mockResolvedValue({}),
440500
},
441501
} as any;
@@ -487,6 +547,45 @@ describe('SignUp', () => {
487547
}),
488548
);
489549
});
550+
551+
it('skips captcha challenge for strategies configured in captcha oauth bypass', async () => {
552+
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
553+
554+
const mockBuildUrlWithAuth = vi.fn().mockReturnValue('https://example.com/sso-callback');
555+
SignUp.clerk = {
556+
buildUrlWithAuth: mockBuildUrlWithAuth,
557+
__internal_environment: {
558+
displayConfig: {
559+
captchaOauthBypass: ['oauth_google'],
560+
},
561+
},
562+
} as any;
563+
564+
const mockFetch = vi.fn().mockResolvedValue({
565+
client: null,
566+
response: {
567+
id: 'signup_123',
568+
verifications: {
569+
externalAccount: {
570+
status: 'complete',
571+
},
572+
},
573+
},
574+
});
575+
BaseResource._fetch = mockFetch;
576+
577+
const signUp = new SignUp();
578+
await signUp.__internal_future.sso({
579+
strategy: 'oauth_google',
580+
redirectUrl: '/complete',
581+
redirectCallbackUrl: '/sso-callback',
582+
});
583+
584+
expect(CaptchaChallenge).not.toHaveBeenCalled();
585+
expect(mockFetch.mock.calls[0]?.[0].body).toHaveProperty('captchaToken', undefined);
586+
expect(mockFetch.mock.calls[0]?.[0].body).toHaveProperty('captchaWidgetType', undefined);
587+
expect(mockFetch.mock.calls[0]?.[0].body).toHaveProperty('captchaError', undefined);
588+
});
490589
});
491590

492591
describe('web3', () => {

0 commit comments

Comments
 (0)