Skip to content
6 changes: 6 additions & 0 deletions .changeset/strict-needles-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

Support `sign_up_if_missing` on SignIn.create, including captcha
105 changes: 101 additions & 4 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
_futureAuthenticateWithPopup,
wrapWithPopupRoutes,
} from '../../utils/authenticateWithPopup';
import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge';
import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask';
import { loadZxcvbn } from '../../utils/zxcvbn';
import {
Expand Down Expand Up @@ -164,12 +165,28 @@ export class SignIn extends BaseResource implements SignInResource {
this.fromJSON(data);
}

create = (params: SignInCreateParams): Promise<SignInResource> => {
create = async (params: SignInCreateParams): Promise<SignInResource> => {
debugLogger.debug('SignIn.create', { id: this.id, strategy: 'strategy' in params ? params.strategy : undefined });
const locale = getBrowserLocale();

let finalParams = { ...params };

// Inject browser locale
const browserLocale = getBrowserLocale();
if (browserLocale) {
finalParams.locale = browserLocale;
}

// Determine captcha requirement based on params
const requiresCaptcha = this.shouldRequireCaptcha(params);

if (requiresCaptcha) {
const captchaParams = await this.getCaptchaToken();
finalParams = { ...finalParams, ...captchaParams };
}

return this._basePost({
path: this.pathRoot,
body: locale ? { locale, ...params } : params,
body: finalParams,
});
};

Expand Down Expand Up @@ -576,6 +593,72 @@ export class SignIn extends BaseResource implements SignInResource {
return this;
}

/**
* We delegate bot detection to the following providers, instead of relying on turnstile exclusively
*
* This is almost identical to SignUp.shouldBypassCaptchaForAttempt, but they differ because on transfer
* sign up needs to check the sign in, and sign in needs to check the sign up.
*/
protected shouldBypassCaptchaForAttempt(params: { strategy?: string; transfer?: boolean }) {
if (!('strategy' in params) || !params.strategy) {
return false;
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const captchaOauthBypass = SignIn.clerk.__internal_environment!.displayConfig.captchaOauthBypass;

if (captchaOauthBypass.some(strategy => strategy === params.strategy)) {
return true;
}

if (
params.transfer &&
captchaOauthBypass.some(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
strategy => strategy === SignIn.clerk.client!.signUp.verifications.externalAccount.strategy,
)
) {
return true;
}

return false;
}

/**
* Determines whether captcha is required based on the provided params.
*/
private shouldRequireCaptcha(params: { strategy?: string; transfer?: boolean; signUpIfMissing?: boolean }): boolean {
// Always bypass for these conditions
if (__BUILD_DISABLE_RHC__) {
return false;
}

if (SignIn.clerk.client?.captchaBypass) {
return false;
}

// Strategy-based bypass (OAuth, etc.)
if (this.shouldBypassCaptchaForAttempt(params)) {
return false;
}

// Require captcha if signUpIfMissing is present
return !!params.signUpIfMissing;
}

/**
* Gets captcha token and widget type from the captcha challenge.
* Throws if captcha is unavailable.
*/
private async getCaptchaToken() {
const captchaChallenge = new CaptchaChallenge(SignIn.clerk);
const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signin' });
if (!captchaParams) {
throw new ClerkRuntimeError('', { code: 'captcha_unavailable' });
}
return captchaParams;
}

public __internal_updateFromJSON(data: SignInJSON | SignInJSONSnapshot | null): this {
return this.fromJSON(data);
}
Expand Down Expand Up @@ -815,10 +898,24 @@ class SignInFuture implements SignInFutureResource {
}

private async _create(params: SignInFutureCreateParams): Promise<void> {
let body = { ...params };

const locale = getBrowserLocale();
if (locale) {
body.locale = locale;
}

// Determine captcha requirement based on params
const requiresCaptcha = this.#resource['shouldRequireCaptcha'](body);

if (requiresCaptcha) {
const captchaParams = await this.#resource['getCaptchaToken']();
body = { ...body, ...captchaParams };
}

await this.#resource.__internal_basePost({
path: this.#resource.pathRoot,
body: locale ? { locale, ...params } : params,
body,
});
}

Expand Down
7 changes: 0 additions & 7 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,6 @@ export class SignUp extends BaseResource implements SignUpResource {
finalParams = { ...finalParams, ...captchaParams };
}

if (finalParams.transfer && this.shouldBypassCaptchaForAttempt(finalParams)) {
const strategy = SignUp.clerk.client?.signIn.firstFactorVerification.strategy;
if (strategy) {
finalParams = { ...finalParams, strategy: strategy as SignUpCreateParams['strategy'] };
}
}

return this._basePost({
path: this.pathRoot,
body: normalizeUnsafeMetadata(finalParams),
Expand Down
Loading
Loading