diff --git a/.changeset/dark-moons-sort.md b/.changeset/dark-moons-sort.md new file mode 100644 index 00000000000..13fdcff2638 --- /dev/null +++ b/.changeset/dark-moons-sort.md @@ -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. \ No newline at end of file diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index c942a77329f..871cc4f63ee 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,10 +1,10 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "610kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "70.2KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "70.3KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "113KB" }, - { "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "108.56KB" }, + { "path": "./dist/clerk.headless*.js", "maxSize": "53.3KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "108.6KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index 453a2bf90de..8a1a27fbe77 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -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; @@ -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, @@ -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, diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 78db8f0b78e..a682df600fd 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -14,6 +14,7 @@ import type { PrepareVerificationParams, PrepareWeb3WalletVerificationParams, SignUpAuthenticateWithWeb3Params, + SignUpCreateOptions, SignUpCreateParams, SignUpField, SignUpIdentificationField, @@ -46,7 +47,7 @@ import { clerkVerifyEmailAddressCalledBeforeCreate, clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; -import { BaseResource, ClerkRuntimeError, SignUpVerifications } from './internal'; +import { BaseResource, SignUpVerifications } from './internal'; declare global { interface Window { @@ -83,16 +84,23 @@ export class SignUp extends BaseResource implements SignUpResource { this.fromJSON(data); } - create = async (_params: SignUpCreateParams): Promise => { + create = async (_params: SignUpCreateParams, options?: SignUpCreateOptions): Promise => { + 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 => { let params: Record = _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)) { @@ -105,6 +113,38 @@ export class SignUp extends BaseResource implements SignUpResource { }); }; + private twoStepCreate = async ( + _params: SignUpCreateParams, + options?: SignUpCreateOptions, + ): Promise => { + const params: Record = _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), + }); + + const isCaptchaChallengeMissingAndRequired = + this.missingFields.some(field => field === 'captcha_challenge') && + this.requiredFields.some(field => field === 'captcha_challenge'); + + if ( + isCaptchaChallengeMissingAndRequired && + !this.shouldBypassCaptchaForAttempt(params) && + !options?.skipCaptchaChallenge + ) { + return this.solveChallenge(); + } + + return this; + }; + prepareVerification = (params: PrepareVerificationParams): Promise => { return this._basePost({ body: params, @@ -438,12 +478,33 @@ export class SignUp extends BaseResource implements SignUpResource { }; } + private solveChallenge = async (): Promise => { + const params = await this.getCaptchaParams(); + if (params) { + return this.update(params); + } + + return this; + }; + + private getCaptchaParams = async (): Promise | undefined> => { + let params: Record | 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) { diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 4cc39f16efe..bfb2590ed3c 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -220,8 +220,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({}, { skipCaptchaChallenge: true })); } } diff --git a/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts b/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts index 57459dc9aa8..aeb5c2d9d48 100644 --- a/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts +++ b/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts @@ -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; } /** diff --git a/packages/types/src/attributes.ts b/packages/types/src/attributes.ts index 7cc23cbcc46..db0c66e6d88 100644 --- a/packages/types/src/attributes.ts +++ b/packages/types/src/attributes.ts @@ -2,3 +2,4 @@ export type FirstNameAttribute = 'first_name'; export type LastNameAttribute = 'last_name'; export type PasswordAttribute = 'password'; export type LegalAcceptedAttribute = 'legal_accepted'; +export type CaptchaChallengeAttribute = 'captcha_challenge'; diff --git a/packages/types/src/displayConfig.ts b/packages/types/src/displayConfig.ts index 9f7c5b07ea5..c80d2b19482 100644 --- a/packages/types/src/displayConfig.ts +++ b/packages/types/src/displayConfig.ts @@ -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; @@ -69,6 +70,7 @@ export interface DisplayConfigResource extends ClerkResource { captchaOauthBypass: OAuthStrategy[]; captchaHeartbeat: boolean; captchaHeartbeatIntervalMs?: number; + twoStepSignUpCreateEnabled?: boolean; homeUrl: string; instanceEnvironmentType: string; logoImageUrl: string; diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index d9485906aa3..ed76722c6f2 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -1,6 +1,12 @@ import type { PhoneCodeChannel } from 'phoneCodeChannel'; -import type { FirstNameAttribute, LastNameAttribute, LegalAcceptedAttribute, PasswordAttribute } from './attributes'; +import type { + CaptchaChallengeAttribute, + FirstNameAttribute, + LastNameAttribute, + LegalAcceptedAttribute, + PasswordAttribute, +} from './attributes'; import type { AttemptEmailAddressVerificationParams, PrepareEmailAddressVerificationParams } from './emailAddress'; import type { EmailAddressIdentifier, @@ -71,7 +77,7 @@ export interface SignUpResource extends ClerkResource { abandonAt: number | null; legalAcceptedAt: number | null; - create: (params: SignUpCreateParams) => Promise; + create: (params: SignUpCreateParams, options?: SignUpCreateOptions) => Promise; update: (params: SignUpUpdateParams) => Promise; @@ -160,7 +166,12 @@ export type AttemptVerificationParams = signature: string; }; -export type SignUpAttributeField = FirstNameAttribute | LastNameAttribute | PasswordAttribute | LegalAcceptedAttribute; +export type SignUpAttributeField = + | FirstNameAttribute + | LastNameAttribute + | PasswordAttribute + | LegalAcceptedAttribute + | CaptchaChallengeAttribute; // TODO: SignUpVerifiableField or SignUpIdentifier? export type SignUpVerifiableField = @@ -199,6 +210,10 @@ export type SignUpCreateParams = Partial< } & Omit>, 'legalAccepted'> >; +export type SignUpCreateOptions = Partial<{ + skipCaptchaChallenge: boolean; +}>; + export type SignUpUpdateParams = SignUpCreateParams; /**