Skip to content

feat(clerk-js): Trigger a new request to submit the captcha token #6076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/dark-moons-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/types': patch
---

Trigger a new request to submit the captcha token on sign up when executing the `signUp.create` method.
6 changes: 6 additions & 0 deletions packages/clerk-js/src/core/resources/DisplayConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource
branded: boolean = false;
captchaHeartbeat: boolean = false;
captchaHeartbeatIntervalMs?: number;
twoStepSignUpCreateEnabled: boolean = false;
captchaOauthBypass: OAuthStrategy[] = ['oauth_google', 'oauth_microsoft', 'oauth_apple'];
captchaProvider: CaptchaProvider = 'turnstile';
captchaPublicKey: string | null = null;
Expand Down Expand Up @@ -80,6 +81,10 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource
this.applicationName = this.withDefault(data.application_name, this.applicationName);
this.branded = this.withDefault(data.branded, this.branded);
this.captchaHeartbeat = this.withDefault(data.captcha_heartbeat, this.captchaHeartbeat);
this.twoStepSignUpCreateEnabled = this.withDefault(
data.two_step_sign_up_create_enabled,
this.twoStepSignUpCreateEnabled,
);
this.captchaHeartbeatIntervalMs = this.withDefault(
data.captcha_heartbeat_interval_ms,
this.captchaHeartbeatIntervalMs,
Expand Down Expand Up @@ -130,6 +135,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource
branded: this.branded,
captcha_heartbeat_interval_ms: this.captchaHeartbeatIntervalMs,
captcha_heartbeat: this.captchaHeartbeat,
two_step_sign_up_create_enabled: this.twoStepSignUpCreateEnabled,
captcha_oauth_bypass: this.captchaOauthBypass,
captcha_provider: this.captchaProvider,
captcha_public_key_invisible: this.captchaPublicKeyInvisible,
Expand Down
69 changes: 61 additions & 8 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
PrepareVerificationParams,
PrepareWeb3WalletVerificationParams,
SignUpAuthenticateWithWeb3Params,
SignUpCreateOptions,
SignUpCreateParams,
SignUpField,
SignUpIdentificationField,
Expand Down Expand Up @@ -46,7 +47,7 @@ import {
clerkVerifyEmailAddressCalledBeforeCreate,
clerkVerifyWeb3WalletCalledBeforeCreate,
} from '../errors';
import { BaseResource, ClerkRuntimeError, SignUpVerifications } from './internal';
import { BaseResource, SignUpVerifications } from './internal';

declare global {
interface Window {
Expand Down Expand Up @@ -83,16 +84,23 @@ export class SignUp extends BaseResource implements SignUpResource {
this.fromJSON(data);
}

create = async (_params: SignUpCreateParams): Promise<SignUpResource> => {
create = async (_params: SignUpCreateParams, options?: SignUpCreateOptions): Promise<SignUpResource> => {
if (SignUp.clerk.__unstable__environment?.displayConfig?.twoStepSignUpCreateEnabled) {
return this.twoStepCreate(_params, options);
}

// This is the old flow and will be completely replaced by the two step flow when it's rolled out to everyone
return this.legacyCreate(_params);
};

private legacyCreate = async (_params: SignUpCreateParams): Promise<SignUpResource> => {
let params: Record<string, unknown> = _params;

if (!__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params)) {
const captchaChallenge = new CaptchaChallenge(SignUp.clerk);
const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signup' });
if (!captchaParams) {
throw new ClerkRuntimeError('', { code: 'captcha_unavailable' });
if (!this.shouldBypassCaptchaForAttempt(params)) {
const captchaParams = await this.getCaptchaParams();
if (captchaParams) {
params = { ...params, ...captchaParams };
}
params = { ...params, ...captchaParams };
}

if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) {
Expand All @@ -105,6 +113,30 @@ export class SignUp extends BaseResource implements SignUpResource {
});
};

private twoStepCreate = async (
_params: SignUpCreateParams,
options?: SignUpCreateOptions,
): Promise<SignUpResource> => {
const params: Record<string, unknown> = _params;

// This is a legacy flow, where we allowed specific OAuth providers to bypass the captcha
// This is no longer supported, but we need to keep it for backwards compatibility
if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) {
params.strategy = SignUp.clerk.client?.signIn.firstFactorVerification.strategy;
}

await this._basePost({
path: this.pathRoot,
body: normalizeUnsafeMetadata(params),
});

if (!this.shouldBypassCaptchaForAttempt(params) && !options?.skipChallenge) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should whether challenge is a missing field factor into this check?

return this.solveChallenge();
}

return this;
};

prepareVerification = (params: PrepareVerificationParams): Promise<this> => {
return this._basePost({
body: params,
Expand Down Expand Up @@ -438,12 +470,33 @@ export class SignUp extends BaseResource implements SignUpResource {
};
}

private solveChallenge = async (): Promise<SignUpResource> => {
const params = await this.getCaptchaParams();
if (params) {
return this.update(params);
}

return this;
};

private getCaptchaParams = async (): Promise<Record<string, unknown> | undefined> => {
let params: Record<string, unknown> | undefined;

if (!__BUILD_DISABLE_RHC__ && !this.clientBypass()) {
const captchaChallenge = new CaptchaChallenge(SignUp.clerk);
params = await captchaChallenge.managedOrInvisible({ action: 'signup' });
}

return params;
};

private clientBypass() {
return SignUp.clerk.client?.captchaBypass;
}

/**
* We delegate bot detection to the following providers, instead of relying on turnstile exclusively
* This is a legacy flow, where we allowed specific OAuth providers to bypass the captcha
*/
protected shouldBypassCaptchaForAttempt(params: SignUpCreateParams) {
if (!params.strategy) {
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ function SignUpStartInternal(): JSX.Element {

// TODO: This is a hack to reset the sign in attempt so that the oauth error
// does not persist on full page reloads.
// We will revise this strategy as part of the Clerk DX epic.
void (await signUp.create({}));
// This will be handled by the backend (FAPI) in the future.
void (await signUp.create({}, { skipChallenge: true }));
}
Comment on lines +218 to 220
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove redundant void (await …) wrapper

Inside an async function you can simply await the promise. The current form is harder to read and offers no benefit.

-        void (await signUp.create({}, { skipChallenge: true }));
+        await signUp.create({}, { skipChallenge: true });

If the result should be ignored but failures tolerated, consider wrapping in try/catch instead of swallowing silently.

🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx around lines 218
to 220, remove the redundant 'void (await ...)' wrapper and simply use 'await'
to handle the promise. If the intention is to ignore the result but still handle
potential errors, wrap the await call in a try/catch block instead of silently
swallowing any failures.

}

Expand Down
9 changes: 4 additions & 5 deletions packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,14 @@ export class CaptchaChallenge {
if (e.captchaError) {
return { captchaError: e.captchaError };
}
// if captcha action is signup, we return undefined, because we don't want to make the call to FAPI
return opts?.action === 'verify' ? { captchaError: e?.message || e || 'unexpected_captcha_error' } : undefined;
return { captchaError: e?.message || e || 'unexpected_captcha_error' };
});
return opts?.action === 'verify' ? { ...captchaResult, captchaAction: 'verify' } : captchaResult;
return { ...captchaResult, captchaAction: opts?.action };
}

// if captcha action is signup, we return an empty object, because it means that the bot protection is disabled
// if captcha action is signup, we return undefined, because it means that the bot protection is disabled
// and the user should be able to sign up without solving a captcha
return opts?.action === 'verify' ? { captchaError: 'captcha_unavailable', captchaAction: opts?.action } : {};
return opts?.action === 'verify' ? { captchaError: 'captcha_unavailable', captchaAction: opts?.action } : undefined;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/displayConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface DisplayConfigJSON {
captcha_oauth_bypass: OAuthStrategy[] | null;
captcha_heartbeat?: boolean;
captcha_heartbeat_interval_ms?: number;
two_step_sign_up_create_enabled?: boolean;
home_url: string;
instance_environment_type: string;
logo_image_url: string;
Expand Down Expand Up @@ -69,6 +70,7 @@ export interface DisplayConfigResource extends ClerkResource {
captchaOauthBypass: OAuthStrategy[];
captchaHeartbeat: boolean;
captchaHeartbeatIntervalMs?: number;
twoStepSignUpCreateEnabled?: boolean;
homeUrl: string;
instanceEnvironmentType: string;
logoImageUrl: string;
Expand Down
6 changes: 5 additions & 1 deletion packages/types/src/signUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface SignUpResource extends ClerkResource {
abandonAt: number | null;
legalAcceptedAt: number | null;

create: (params: SignUpCreateParams) => Promise<SignUpResource>;
create: (params: SignUpCreateParams, options?: SignUpCreateOptions) => Promise<SignUpResource>;

update: (params: SignUpUpdateParams) => Promise<SignUpResource>;

Expand Down Expand Up @@ -199,6 +199,10 @@ export type SignUpCreateParams = Partial<
} & Omit<SnakeToCamel<Record<SignUpAttributeField | SignUpVerifiableField, string>>, 'legalAccepted'>
>;

export type SignUpCreateOptions = Partial<{
skipChallenge: boolean;
}>;

export type SignUpUpdateParams = SignUpCreateParams;

/**
Expand Down