Skip to content

Commit edd9d86

Browse files
authored
Merge branch 'main' into chris/mobile-460-add-two-way-jsnative-session-sync-for-expo-components
2 parents 3b1d3ee + de1386f commit edd9d86

8 files changed

Lines changed: 770 additions & 39 deletions

File tree

.changeset/strict-needles-taste.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/shared': minor
4+
---
5+
6+
Support `sign_up_if_missing` on SignIn.create, including captcha

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

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
AuthenticateWithPopupParams,
1717
AuthenticateWithRedirectParams,
1818
AuthenticateWithWeb3Params,
19+
CaptchaWidgetType,
1920
ClientTrustState,
2021
CreateEmailLinkFlowReturn,
2122
EmailCodeConfig,
@@ -82,6 +83,7 @@ import {
8283
_futureAuthenticateWithPopup,
8384
wrapWithPopupRoutes,
8485
} from '../../utils/authenticateWithPopup';
86+
import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge';
8587
import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask';
8688
import { loadZxcvbn } from '../../utils/zxcvbn';
8789
import {
@@ -164,12 +166,34 @@ export class SignIn extends BaseResource implements SignInResource {
164166
this.fromJSON(data);
165167
}
166168

167-
create = (params: SignInCreateParams): Promise<SignInResource> => {
169+
create = async (params: SignInCreateParams): Promise<SignInResource> => {
168170
debugLogger.debug('SignIn.create', { id: this.id, strategy: 'strategy' in params ? params.strategy : undefined });
169-
const locale = getBrowserLocale();
171+
172+
let body: Record<string, unknown> = { ...params };
173+
174+
// Inject browser locale
175+
const browserLocale = getBrowserLocale();
176+
if (browserLocale) {
177+
body.locale = browserLocale;
178+
}
179+
180+
if (
181+
this.shouldRequireCaptcha(params) &&
182+
!__BUILD_DISABLE_RHC__ &&
183+
!this.clientBypass() &&
184+
!this.shouldBypassCaptchaForAttempt(params)
185+
) {
186+
const captchaChallenge = new CaptchaChallenge(SignIn.clerk);
187+
const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signin' });
188+
if (!captchaParams) {
189+
throw new ClerkRuntimeError('', { code: 'captcha_unavailable' });
190+
}
191+
body = { ...body, ...captchaParams };
192+
}
193+
170194
return this._basePost({
171195
path: this.pathRoot,
172-
body: locale ? { locale, ...params } : params,
196+
body: body,
173197
});
174198
};
175199

@@ -576,6 +600,43 @@ export class SignIn extends BaseResource implements SignInResource {
576600
return this;
577601
}
578602

603+
private clientBypass() {
604+
return SignIn.clerk.client?.captchaBypass;
605+
}
606+
607+
/**
608+
* Determines whether captcha is required for sign in based on the provided params.
609+
* Add new conditions here as captcha requirements evolve.
610+
*/
611+
private shouldRequireCaptcha(params: SignInCreateParams): boolean {
612+
if ('signUpIfMissing' in params && params.signUpIfMissing) {
613+
return true;
614+
}
615+
616+
return false;
617+
}
618+
619+
/**
620+
* We delegate bot detection to the following providers, instead of relying on turnstile exclusively
621+
*/
622+
protected shouldBypassCaptchaForAttempt(params: SignInCreateParams) {
623+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
624+
const captchaOauthBypass = SignIn.clerk.__internal_environment!.displayConfig.captchaOauthBypass;
625+
626+
// Check transfer strategy against bypass list
627+
if (params.transfer && SignIn.clerk.client?.signUp?.verifications?.externalAccount?.status === 'transferable') {
628+
const signUpStrategy = SignIn.clerk.client.signUp.verifications.externalAccount.strategy;
629+
return signUpStrategy ? captchaOauthBypass.some(strategy => strategy === signUpStrategy) : false;
630+
}
631+
632+
// Check direct strategy against bypass list
633+
if ('strategy' in params && params.strategy) {
634+
return captchaOauthBypass.some(strategy => strategy === params.strategy);
635+
}
636+
637+
return false;
638+
}
639+
579640
public __internal_updateFromJSON(data: SignInJSON | SignInJSONSnapshot | null): this {
580641
return this.fromJSON(data);
581642
}
@@ -814,11 +875,80 @@ class SignInFuture implements SignInFutureResource {
814875
});
815876
}
816877

878+
/**
879+
* Determines whether captcha is required for sign in based on the provided params.
880+
* Add new conditions here as captcha requirements evolve.
881+
*/
882+
private shouldRequireCaptcha(params: { signUpIfMissing?: boolean }): boolean {
883+
if (params.signUpIfMissing) {
884+
return true;
885+
}
886+
887+
return false;
888+
}
889+
890+
private shouldBypassCaptchaForAttempt(params: { strategy?: string; transfer?: boolean }) {
891+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
892+
const captchaOauthBypass = SignIn.clerk.__internal_environment!.displayConfig.captchaOauthBypass;
893+
894+
// Check transfer strategy against bypass list
895+
if (params.transfer && SignIn.clerk.client?.signUp?.verifications?.externalAccount?.status === 'transferable') {
896+
const signUpStrategy = SignIn.clerk.client.signUp.verifications.externalAccount.strategy;
897+
return signUpStrategy ? captchaOauthBypass.some(strategy => strategy === signUpStrategy) : false;
898+
}
899+
900+
// Check direct strategy against bypass list
901+
if (params.strategy) {
902+
return captchaOauthBypass.some(strategy => strategy === params.strategy);
903+
}
904+
905+
return false;
906+
}
907+
908+
private async getCaptchaToken(
909+
params: { strategy?: string; transfer?: boolean; signUpIfMissing?: boolean } = {},
910+
): Promise<{
911+
captchaToken?: string;
912+
captchaWidgetType?: CaptchaWidgetType;
913+
captchaError?: unknown;
914+
}> {
915+
if (
916+
!this.shouldRequireCaptcha(params) ||
917+
__BUILD_DISABLE_RHC__ ||
918+
SignIn.clerk.client?.captchaBypass ||
919+
this.shouldBypassCaptchaForAttempt(params)
920+
) {
921+
return {
922+
captchaToken: undefined,
923+
captchaWidgetType: undefined,
924+
captchaError: undefined,
925+
};
926+
}
927+
928+
const captchaChallenge = new CaptchaChallenge(SignIn.clerk);
929+
const response = await captchaChallenge.managedOrInvisible({ action: 'signin' });
930+
if (!response) {
931+
throw new Error('Captcha challenge failed');
932+
}
933+
934+
const { captchaError, captchaToken, captchaWidgetType } = response;
935+
return { captchaToken, captchaWidgetType, captchaError };
936+
}
937+
817938
private async _create(params: SignInFutureCreateParams): Promise<void> {
818-
const locale = getBrowserLocale();
939+
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(params);
940+
941+
const body: Record<string, unknown> = {
942+
...params,
943+
captchaToken,
944+
captchaWidgetType,
945+
captchaError,
946+
locale: getBrowserLocale() || undefined,
947+
};
948+
819949
await this.#resource.__internal_basePost({
820950
path: this.#resource.pathRoot,
821-
body: locale ? { locale, ...params } : params,
951+
body,
822952
});
823953
}
824954

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

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,6 @@ export class SignUp extends BaseResource implements SignUpResource {
173173
finalParams = { ...finalParams, ...captchaParams };
174174
}
175175

176-
if (finalParams.transfer && this.shouldBypassCaptchaForAttempt(finalParams)) {
177-
const strategy = SignUp.clerk.client?.signIn.firstFactorVerification.strategy;
178-
if (strategy) {
179-
finalParams = { ...finalParams, strategy: strategy as SignUpCreateParams['strategy'] };
180-
}
181-
}
182-
183176
return this._basePost({
184177
path: this.pathRoot,
185178
body: normalizeUnsafeMetadata(finalParams),
@@ -561,22 +554,30 @@ export class SignUp extends BaseResource implements SignUpResource {
561554
* We delegate bot detection to the following providers, instead of relying on turnstile exclusively
562555
*/
563556
protected shouldBypassCaptchaForAttempt(params: SignUpCreateParams) {
564-
if (!params.strategy) {
565-
return false;
566-
}
567-
568557
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
569558
const captchaOauthBypass = SignUp.clerk.__internal_environment!.displayConfig.captchaOauthBypass;
570559

571-
if (captchaOauthBypass.some(strategy => strategy === params.strategy)) {
572-
return true;
560+
// Check for transfer captcha bypass.
561+
if (params.transfer) {
562+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
563+
const signInVerificationStrategy = SignUp.clerk.client!.signIn.firstFactorVerification.strategy;
564+
565+
// OAuth transfers: If we delegate captcha detection to OAuth provider,
566+
// do not show another captcha on sign up.
567+
if (captchaOauthBypass.some(strategy => strategy === signInVerificationStrategy)) {
568+
return true;
569+
}
570+
571+
// Sign up if missing transfers: We let sign in handle the captcha,
572+
// do not show another captcha on sign up.
573+
if (isSignUpIfMissingCaptchaBypassStrategy(signInVerificationStrategy)) {
574+
return true;
575+
}
573576
}
574577

575-
if (
576-
params.transfer &&
577-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
578-
captchaOauthBypass.some(strategy => strategy === SignUp.clerk.client!.signIn.firstFactorVerification.strategy)
579-
) {
578+
// OAuth sign ups: If we delegate captcha detection to OAuth provider,
579+
// do not show another captcha on sign up.
580+
if (params.strategy && captchaOauthBypass.some(strategy => strategy === params.strategy)) {
580581
return true;
581582
}
582583

@@ -595,6 +596,22 @@ export class SignUp extends BaseResource implements SignUpResource {
595596
};
596597
}
597598

599+
/**
600+
* Returns true if the given strategy is one where captcha is already handled
601+
* by the sign-in attempt with sign up if missing, so a subsequent sign-up should
602+
* not show another captcha. Matches email_link, email_code, phone_code, and any
603+
* web3 wallet strategy. This should be kept in sync with `validateSignUpIfMissing`
604+
* in the backend.
605+
*/
606+
const SIGN_UP_IF_MISSING_CAPTCHA_BYPASS_STRATEGIES = new Set(['email_link', 'email_code', 'phone_code']);
607+
608+
export function isSignUpIfMissingCaptchaBypassStrategy(strategy: string | null): boolean {
609+
if (!strategy) {
610+
return false;
611+
}
612+
return SIGN_UP_IF_MISSING_CAPTCHA_BYPASS_STRATEGIES.has(strategy) || strategy.startsWith('web3_');
613+
}
614+
598615
type SignUpFutureVerificationsMethods = Pick<
599616
SignUpFutureVerifications,
600617
| 'sendEmailCode'
@@ -787,22 +804,30 @@ class SignUpFuture implements SignUpFutureResource {
787804
}
788805

789806
private shouldBypassCaptchaForAttempt(params: { strategy?: string; transfer?: boolean }) {
790-
if (!params.strategy) {
791-
return false;
792-
}
793-
794807
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
795808
const captchaOauthBypass = SignUp.clerk.__internal_environment!.displayConfig.captchaOauthBypass;
796809

797-
if (captchaOauthBypass.some(strategy => strategy === params.strategy)) {
798-
return true;
810+
// Check for transfer captcha bypass.
811+
if (params.transfer) {
812+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
813+
const signInVerificationStrategy = SignUp.clerk.client!.signIn.firstFactorVerification.strategy;
814+
815+
// OAuth transfers: If we delegate captcha detection to OAuth provider,
816+
// do not show another captcha on sign up.
817+
if (captchaOauthBypass.some(strategy => strategy === signInVerificationStrategy)) {
818+
return true;
819+
}
820+
821+
// Sign up if missing transfers: We let sign in handle the captcha,
822+
// do not show another captcha on sign up.
823+
if (isSignUpIfMissingCaptchaBypassStrategy(signInVerificationStrategy)) {
824+
return true;
825+
}
799826
}
800827

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-
) {
828+
// OAuth sign ups: If we delegate captcha detection to OAuth provider,
829+
// do not show another captcha on sign up.
830+
if (params.strategy && captchaOauthBypass.some(strategy => strategy === params.strategy)) {
806831
return true;
807832
}
808833

0 commit comments

Comments
 (0)