From d8fdebb05cc022c01733ca01614bcd4e4b1e8b39 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Mon, 14 Oct 2024 16:40:49 +0300 Subject: [PATCH 1/7] feat(types,clerk-js): Bypass captcha for providers dynamically provided in environment (#4322) --- .changeset/shy-peaches-grow.md | 6 ++++++ .../src/core/resources/DisplayConfig.ts | 5 +++++ packages/clerk-js/src/core/resources/SignUp.ts | 17 +++++++++-------- packages/types/src/displayConfig.ts | 8 ++++++++ 4 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 .changeset/shy-peaches-grow.md diff --git a/.changeset/shy-peaches-grow.md b/.changeset/shy-peaches-grow.md new file mode 100644 index 00000000000..923fd722f0b --- /dev/null +++ b/.changeset/shy-peaches-grow.md @@ -0,0 +1,6 @@ +--- +"@clerk/clerk-js": patch +"@clerk/types": patch +--- + +Bypass captcha for providers dynamically provided in environment diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index d98f5bf9a83..53e22beb391 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -5,6 +5,7 @@ import type { DisplayConfigJSON, DisplayConfigResource, DisplayThemeJSON, + OAuthStrategy, PreferredSignInStrategy, } from '@clerk/types'; @@ -25,6 +26,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource captchaWidgetType: CaptchaWidgetType = null; captchaProvider: CaptchaProvider = 'turnstile'; captchaPublicKeyInvisible: string | null = null; + captchaOauthBypass: OAuthStrategy[] = []; homeUrl!: string; instanceEnvironmentType!: string; faviconImageUrl!: string; @@ -84,6 +86,9 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource this.captchaWidgetType = data.captcha_widget_type; this.captchaProvider = data.captcha_provider; this.captchaPublicKeyInvisible = data.captcha_public_key_invisible; + // These are the OAuth strategies we used to bypass the captcha for by default + // before the introduction of the captcha_oauth_bypass field + this.captchaOauthBypass = data.captcha_oauth_bypass || ['oauth_google', 'oauth_microsoft', 'oauth_apple']; this.supportEmail = data.support_email || ''; this.clerkJSVersion = data.clerk_js_version; this.organizationProfileUrl = data.organization_profile_url; diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index f7902bedb47..ffd8f90e078 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -339,18 +339,19 @@ export class SignUp extends BaseResource implements SignUpResource { * We delegate bot detection to the following providers, instead of relying on turnstile exclusively */ protected shouldBypassCaptchaForAttempt(params: SignUpCreateParams) { - if ( - params.strategy === 'oauth_google' || - params.strategy === 'oauth_microsoft' || - params.strategy === 'oauth_apple' - ) { + if (!params.strategy) { + return false; + } + + const captchaOauthBypass = SignUp.clerk.__unstable__environment!.displayConfig.captchaOauthBypass; + + if (captchaOauthBypass.some(strategy => strategy === params.strategy)) { return true; } + if ( params.transfer && - (SignUp.clerk.client?.signIn.firstFactorVerification.strategy === 'oauth_google' || - SignUp.clerk.client?.signIn.firstFactorVerification.strategy === 'oauth_microsoft' || - SignUp.clerk.client?.signIn.firstFactorVerification.strategy === 'oauth_apple') + captchaOauthBypass.some(strategy => strategy === SignUp.clerk.client!.signIn.firstFactorVerification.strategy) ) { return true; } diff --git a/packages/types/src/displayConfig.ts b/packages/types/src/displayConfig.ts index d6fed83e52f..8c495f9dd83 100644 --- a/packages/types/src/displayConfig.ts +++ b/packages/types/src/displayConfig.ts @@ -1,5 +1,6 @@ import type { DisplayThemeJSON } from './json'; import type { ClerkResource } from './resource'; +import type { OAuthStrategy } from './strategies'; export type PreferredSignInStrategy = 'password' | 'otp'; export type CaptchaWidgetType = 'smart' | 'invisible' | null; @@ -19,6 +20,7 @@ export interface DisplayConfigJSON { captcha_widget_type: CaptchaWidgetType; captcha_public_key_invisible: string | null; captcha_provider: CaptchaProvider; + captcha_oauth_bypass: OAuthStrategy[] | null; home_url: string; instance_environment_type: string; /* @deprecated */ @@ -55,6 +57,12 @@ export interface DisplayConfigResource extends ClerkResource { captchaWidgetType: CaptchaWidgetType; captchaProvider: CaptchaProvider; captchaPublicKeyInvisible: string | null; + /** + * An array of OAuth strategies for which we will bypass the captcha. + * We trust that the provider will verify that the user is not a bot on their end. + * This can also be used to bypass the captcha for a specific OAuth provider on a per-instance basis. + */ + captchaOauthBypass: OAuthStrategy[]; homeUrl: string; instanceEnvironmentType: string; logoImageUrl: string; From eed4b09ba3ffe07be3b11ec5205b0013e4f26ded Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 14 Oct 2024 18:07:17 +0300 Subject: [PATCH 2/7] fix(clerk-js): Add support for loading Turnstile from Cloudflare host (#4321) --- .changeset/odd-squids-dress.md | 7 ++++ .../clerk-js/src/utils/captcha/turnstile.ts | 32 +++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 .changeset/odd-squids-dress.md diff --git a/.changeset/odd-squids-dress.md b/.changeset/odd-squids-dress.md new file mode 100644 index 00000000000..04e8745bdee --- /dev/null +++ b/.changeset/odd-squids-dress.md @@ -0,0 +1,7 @@ +--- +"@clerk/clerk-js": patch +--- + +Improve bot detection by loading the Turnstile SDK directly from CloudFlare. + +If loading fails due to CSP rules, load it through FAPI instead. diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index ce63d283a4a..2e763e19c21 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -3,6 +3,8 @@ import type { CaptchaWidgetType } from '@clerk/types'; import { CAPTCHA_ELEMENT_ID, CAPTCHA_INVISIBLE_CLASSNAME } from './constants'; +const CLOUDFLARE_TURNSTILE_ORIGINAL_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js'; + interface RenderOptions { /** * Every widget has a sitekey. This sitekey is associated with the corresponding widget configuration and is created upon the widget creation. @@ -69,21 +71,31 @@ export const shouldRetryTurnstileErrorCode = (errorCode: string) => { return !!codesWithRetries.find(w => errorCode.startsWith(w)); }; -async function loadCaptcha(url: string) { +async function loadCaptcha(fallbackUrl: string) { if (!window.turnstile) { - try { - await loadScript(url, { defer: true }); - } catch { - // Rethrow with specific message - console.error('Clerk: Failed to load the CAPTCHA script from the URL: ', url); - throw { - captchaError: 'captcha_script_failed_to_load', - }; - } + await loadCaptchaFromCloudflareURL() + .catch(() => loadCaptchaFromFAPIProxiedURL(fallbackUrl)) + .catch(() => { + throw { captchaError: 'captcha_script_failed_to_load' }; + }); } return window.turnstile; } +async function loadCaptchaFromCloudflareURL() { + return await loadScript(CLOUDFLARE_TURNSTILE_ORIGINAL_URL, { defer: true }); +} + +async function loadCaptchaFromFAPIProxiedURL(fallbackUrl: string) { + try { + return await loadScript(fallbackUrl, { defer: true }); + } catch (err) { + // Rethrow with specific message + console.error('Clerk: Failed to load the CAPTCHA script from the URL: ', fallbackUrl); + throw err; + } +} + /* * How this function works: * The widgetType is either 'invisible' or 'smart'. From 5589a5dc5686906360d40507aaf6b428b845e716 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 14 Oct 2024 19:39:21 +0300 Subject: [PATCH 3/7] fix(clerk-js): Add Clerk docs link when loading Turnstile throws an error (#4325) --- .changeset/quick-months-rest.md | 2 ++ packages/clerk-js/src/utils/captcha/turnstile.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/quick-months-rest.md diff --git a/.changeset/quick-months-rest.md b/.changeset/quick-months-rest.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/quick-months-rest.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index 2e763e19c21..359dc311a07 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -83,7 +83,14 @@ async function loadCaptcha(fallbackUrl: string) { } async function loadCaptchaFromCloudflareURL() { - return await loadScript(CLOUDFLARE_TURNSTILE_ORIGINAL_URL, { defer: true }); + try { + return await loadScript(CLOUDFLARE_TURNSTILE_ORIGINAL_URL, { defer: true }); + } catch (err) { + console.warn( + 'Clerk: Failed to load the CAPTCHA script from Clouflare. If you see a CSP error in your browser, please add the necessary CSP rules to your app. Visit https://clerk.com/docs/security/clerk-csp for more information.', + ); + throw err; + } } async function loadCaptchaFromFAPIProxiedURL(fallbackUrl: string) { From d96034f5ef55bfb37d0d4ea7e6840463cdda4eac Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Mon, 14 Oct 2024 21:45:51 +0300 Subject: [PATCH 4/7] fix(clerk-js): Reload env and retry if authenticateWithRedirect fails (#4327) --- .changeset/itchy-birds-juggle.md | 2 ++ .../src/core/resources/Environment.ts | 2 +- .../clerk-js/src/core/resources/SignUp.ts | 26 ++++++++++++------- 3 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 .changeset/itchy-birds-juggle.md diff --git a/.changeset/itchy-birds-juggle.md b/.changeset/itchy-birds-juggle.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/itchy-birds-juggle.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index 9ccee532d76..b8594336679 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -33,7 +33,7 @@ export class Environment extends BaseResource implements EnvironmentResource { this.fromJSON(data); } - fetch({ touch = false }: { touch: boolean }): Promise { + fetch({ touch }: { touch: boolean } = { touch: false }): Promise { if (touch) { return this._basePatch({}); } diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index ffd8f90e078..946659d9f3c 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -275,15 +275,23 @@ export class SignUp extends BaseResource implements SignUpResource { }: AuthenticateWithRedirectParams & { unsafeMetadata?: SignUpUnsafeMetadata; }): Promise => { - const authenticateFn = (args: SignUpCreateParams | SignUpUpdateParams) => - continueSignUp && this.id ? this.update(args) : this.create(args); - - const { verifications } = await authenticateFn({ - strategy, - redirectUrl: SignUp.clerk.buildUrlWithAuth(redirectUrl), - actionCompleteRedirectUrl: redirectUrlComplete, - unsafeMetadata, - emailAddress, + const authenticateFn = () => { + const params = { + strategy, + redirectUrl: SignUp.clerk.buildUrlWithAuth(redirectUrl), + actionCompleteRedirectUrl: redirectUrlComplete, + unsafeMetadata, + emailAddress, + }; + return continueSignUp && this.id ? this.update(params) : this.create(params); + }; + + const { verifications } = await authenticateFn().catch(async () => { + // If captcha verification failed because the environment has changed, we need + // to reload the environment and try again one more time with the new environment. + // If this fails again, we will let the caller handle the error accordingly. + await SignUp.clerk.__unstable__environment!.reload(); + return authenticateFn(); }); const { externalAccount } = verifications; From bdf2a84478b3a4a08f23e1c53e5ab196d411a084 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Tue, 15 Oct 2024 01:10:55 +0300 Subject: [PATCH 5/7] fix(clerk-js): Only retry oauth if captcha failed (#4329) --- .changeset/nasty-melons-cross.md | 6 ++++++ packages/clerk-js/src/core/resources/SignUp.ts | 12 ++++++++---- packages/shared/src/error.ts | 4 ++++ 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .changeset/nasty-melons-cross.md diff --git a/.changeset/nasty-melons-cross.md b/.changeset/nasty-melons-cross.md new file mode 100644 index 00000000000..e0c69c28bc3 --- /dev/null +++ b/.changeset/nasty-melons-cross.md @@ -0,0 +1,6 @@ +--- +"@clerk/clerk-js": patch +"@clerk/shared": patch +--- + +Only retry the OAuth flow if the captcha check failed. diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 946659d9f3c..6f4d098c0b6 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -1,4 +1,5 @@ -import { deprecated, Poller } from '@clerk/shared'; +import { Poller } from '@clerk/shared'; +import { isCaptchaError, isClerkAPIResponseError } from '@clerk/shared/error'; import type { AttemptEmailAddressVerificationParams, AttemptPhoneNumberVerificationParams, @@ -286,12 +287,15 @@ export class SignUp extends BaseResource implements SignUpResource { return continueSignUp && this.id ? this.update(params) : this.create(params); }; - const { verifications } = await authenticateFn().catch(async () => { + const { verifications } = await authenticateFn().catch(async e => { // If captcha verification failed because the environment has changed, we need // to reload the environment and try again one more time with the new environment. // If this fails again, we will let the caller handle the error accordingly. - await SignUp.clerk.__unstable__environment!.reload(); - return authenticateFn(); + if (isClerkAPIResponseError(e) && isCaptchaError(e)) { + await SignUp.clerk.__unstable__environment!.reload(); + return authenticateFn(); + } + throw e; }); const { externalAccount } = verifications; diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 4c8e574bad6..6ed04152bce 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -8,6 +8,10 @@ export function isUnauthorizedError(e: any): boolean { return code === 'authentication_invalid' && status === 401; } +export function isCaptchaError(e: ClerkAPIResponseError): boolean { + return ['captcha_invalid', 'captcha_not_enabled', 'captcha_missing_token'].includes(e.errors[0].code); +} + export function is4xxError(e: any): boolean { const status = e?.status; return !!status && status >= 400 && status < 500; From b996c72e5b01a2ffcfdb61b4619ec214e7329710 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Tue, 15 Oct 2024 13:50:09 +0300 Subject: [PATCH 6/7] fix(clerk-js): Fix import --- packages/clerk-js/src/core/resources/SignUp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 6f4d098c0b6..52b2983f707 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -1,4 +1,4 @@ -import { Poller } from '@clerk/shared'; +import { deprecated, Poller } from '@clerk/shared'; import { isCaptchaError, isClerkAPIResponseError } from '@clerk/shared/error'; import type { AttemptEmailAddressVerificationParams, From 0f9a20592a8b6cd04dec9cf7b7cd4402596b3891 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Tue, 15 Oct 2024 15:22:27 +0300 Subject: [PATCH 7/7] fix(clerk-js): Fix typo --- packages/clerk-js/src/utils/captcha/turnstile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index 359dc311a07..33a72a570a1 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -87,7 +87,7 @@ async function loadCaptchaFromCloudflareURL() { return await loadScript(CLOUDFLARE_TURNSTILE_ORIGINAL_URL, { defer: true }); } catch (err) { console.warn( - 'Clerk: Failed to load the CAPTCHA script from Clouflare. If you see a CSP error in your browser, please add the necessary CSP rules to your app. Visit https://clerk.com/docs/security/clerk-csp for more information.', + 'Clerk: Failed to load the CAPTCHA script from Cloudflare. If you see a CSP error in your browser, please add the necessary CSP rules to your app. Visit https://clerk.com/docs/security/clerk-csp for more information.', ); throw err; }