From 5faa6e715e461738b7dcc9c9ac24afeddbf8d33b Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 15 Apr 2024 15:34:52 +0300 Subject: [PATCH 1/4] feat(clerk-js): Fallback to invisible CAPTCHA if the element to render to is not found in the DOM --- .changeset/itchy-onions-rush.md | 6 ++++ .../src/core/resources/DisplayConfig.ts | 2 ++ .../clerk-js/src/core/resources/SignUp.ts | 10 ++++-- packages/clerk-js/src/utils/captcha.ts | 31 ++++++++++++++----- .../clerk-js/src/utils/retrieveCaptchaInfo.ts | 1 + packages/types/src/displayConfig.ts | 4 ++- 6 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 .changeset/itchy-onions-rush.md diff --git a/.changeset/itchy-onions-rush.md b/.changeset/itchy-onions-rush.md new file mode 100644 index 0000000000..232f48f871 --- /dev/null +++ b/.changeset/itchy-onions-rush.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Fallback to invisible CAPTCHA if the element to render to is not found in the DOM diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index 222aad45fc..cb23e0e97c 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -21,6 +21,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource branded!: boolean; captchaPublicKey: string | null = null; captchaWidgetType: CaptchaWidgetType = null; + captchaPublicKeyInvisible: string | null = null; homeUrl!: string; instanceEnvironmentType!: string; faviconImageUrl!: string; @@ -67,6 +68,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource this.branded = data.branded; this.captchaPublicKey = data.captcha_public_key; this.captchaWidgetType = data.captcha_widget_type; + this.captchaPublicKeyInvisible = data.captcha_public_key_invisible; 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 7d1def63bb..885699fc9c 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -68,15 +68,19 @@ export class SignUp extends BaseResource implements SignUpResource { create = async (params: SignUpCreateParams): Promise => { const paramsWithCaptcha: Record = params; - const { captchaSiteKey, canUseCaptcha, captchaURL, captchaWidgetType } = retrieveCaptchaInfo(SignUp.clerk); + const { captchaSiteKey, canUseCaptcha, captchaURL, captchaWidgetType, captchaPublicKeyInvisible } = + retrieveCaptchaInfo(SignUp.clerk); - if (canUseCaptcha && captchaSiteKey && captchaURL) { + if (canUseCaptcha && captchaSiteKey && captchaURL && captchaPublicKeyInvisible) { try { - paramsWithCaptcha.captchaToken = await getCaptchaToken({ + const { captchaToken, captchaWidgetTypeUsed } = await getCaptchaToken({ siteKey: captchaSiteKey, widgetType: captchaWidgetType, + invisibleSiteKey: captchaPublicKeyInvisible, scriptUrl: captchaURL, }); + paramsWithCaptcha.captchaToken = captchaToken; + paramsWithCaptcha.captchaWidgetType = captchaWidgetTypeUsed; } catch (e) { if (e.captchaError) { paramsWithCaptcha.captchaError = e.captchaError; diff --git a/packages/clerk-js/src/utils/captcha.ts b/packages/clerk-js/src/utils/captcha.ts index 60b9ebe116..f6c93895f4 100644 --- a/packages/clerk-js/src/utils/captcha.ts +++ b/packages/clerk-js/src/utils/captcha.ts @@ -36,6 +36,10 @@ interface RenderOptions { * @param errorCode string */ 'error-callback'?: (errorCode: string) => void; + /** + * A JavaScript callback invoked when a given client/browser is not supported by the widget. + */ + 'unsupported-callback'?: () => boolean; /** * Appearance controls when the widget is visible. * It can be always (default), execute, or interaction-only. @@ -84,28 +88,35 @@ export const getCaptchaToken = async (captchaOptions: { siteKey: string; scriptUrl: string; widgetType: CaptchaWidgetType; + invisibleSiteKey: string; }) => { const { siteKey: sitekey, scriptUrl, widgetType } = captchaOptions; let captchaToken = '', id = ''; - const invisibleWidget = !widgetType || widgetType === 'invisible'; + let invisibleWidget = !widgetType || widgetType === 'invisible'; let widgetDiv: HTMLElement | null = null; - if (invisibleWidget) { + const createInvisibleDOMElement = () => { const div = document.createElement('div'); div.classList.add(CAPTCHA_INVISIBLE_CLASSNAME); document.body.appendChild(div); - widgetDiv = div; + return div; + }; + + if (invisibleWidget) { + widgetDiv = createInvisibleDOMElement(); } else { const visibleDiv = document.getElementById(CAPTCHA_ELEMENT_ID); if (visibleDiv) { visibleDiv.style.display = 'block'; widgetDiv = visibleDiv; } else { - throw { - captchaError: 'Element to render the captcha not found', - }; + console.error( + 'Captcha DOM element not found. Using invisible captcha widget. Learm more in the docs here: test.link', + ); + widgetDiv = createInvisibleDOMElement(); + invisibleWidget = true; } } @@ -118,7 +129,7 @@ export const getCaptchaToken = async (captchaOptions: { try { const id = captcha.render(invisibleWidget ? `.${CAPTCHA_INVISIBLE_CLASSNAME}` : `#${CAPTCHA_ELEMENT_ID}`, { sitekey, - appearance: widgetType === 'always_visible' ? 'always' : 'interaction-only', + appearance: 'interaction-only', retry: 'never', 'refresh-expired': 'auto', callback: function (token: string) { @@ -139,6 +150,10 @@ export const getCaptchaToken = async (captchaOptions: { } reject([errorCodes.join(','), id]); }, + 'unsupported-callback': function () { + reject(['This browser is unsupported by the CAPTCHA.', id]); + return true; + }, }); } catch (e) { /** @@ -171,5 +186,5 @@ export const getCaptchaToken = async (captchaOptions: { } } - return captchaToken; + return { captchaToken, captchaWidgetTypeUsed: invisibleWidget ? 'invisible' : 'smart' }; }; diff --git a/packages/clerk-js/src/utils/retrieveCaptchaInfo.ts b/packages/clerk-js/src/utils/retrieveCaptchaInfo.ts index d83f4a4d12..ca4d4eb5d8 100644 --- a/packages/clerk-js/src/utils/retrieveCaptchaInfo.ts +++ b/packages/clerk-js/src/utils/retrieveCaptchaInfo.ts @@ -7,6 +7,7 @@ export const retrieveCaptchaInfo = (clerk: Clerk) => { return { captchaSiteKey: _environment ? _environment.displayConfig.captchaPublicKey : null, captchaWidgetType: _environment ? _environment.displayConfig.captchaWidgetType : null, + captchaPublicKeyInvisible: _environment ? _environment.displayConfig.captchaPublicKeyInvisible : null, canUseCaptcha: _environment ? _environment.userSettings.signUp.captcha_enabled && clerk.isStandardBrowser && diff --git a/packages/types/src/displayConfig.ts b/packages/types/src/displayConfig.ts index c7039c82ef..0ebd8e3c8d 100644 --- a/packages/types/src/displayConfig.ts +++ b/packages/types/src/displayConfig.ts @@ -2,7 +2,7 @@ import type { DisplayThemeJSON } from './json'; import type { ClerkResource } from './resource'; export type PreferredSignInStrategy = 'password' | 'otp'; -export type CaptchaWidgetType = 'smart' | 'always_visible' | 'invisible' | null; +export type CaptchaWidgetType = 'smart' | 'invisible' | null; export interface DisplayConfigJSON { object: 'display_config'; @@ -16,6 +16,7 @@ export interface DisplayConfigJSON { branded: boolean; captcha_public_key: string | null; captcha_widget_type: CaptchaWidgetType; + captcha_public_key_invisible: string | null; home_url: string; instance_environment_type: string; logo_image_url: string; @@ -45,6 +46,7 @@ export interface DisplayConfigResource extends ClerkResource { branded: boolean; captchaPublicKey: string | null; captchaWidgetType: CaptchaWidgetType; + captchaPublicKeyInvisible: string | null; homeUrl: string; instanceEnvironmentType: string; logoImageUrl: string; From a758c911b32559d3dbde439cee7602944d22be1f Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 15 Apr 2024 17:23:55 +0300 Subject: [PATCH 2/4] fix(clerk-js): Fix error wording --- packages/clerk-js/src/utils/captcha.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/utils/captcha.ts b/packages/clerk-js/src/utils/captcha.ts index f6c93895f4..bfc74330c3 100644 --- a/packages/clerk-js/src/utils/captcha.ts +++ b/packages/clerk-js/src/utils/captcha.ts @@ -112,9 +112,7 @@ export const getCaptchaToken = async (captchaOptions: { visibleDiv.style.display = 'block'; widgetDiv = visibleDiv; } else { - console.error( - 'Captcha DOM element not found. Using invisible captcha widget. Learm more in the docs here: test.link', - ); + console.error('Captcha DOM element not found. Using invisible captcha widget.'); widgetDiv = createInvisibleDOMElement(); invisibleWidget = true; } @@ -151,7 +149,7 @@ export const getCaptchaToken = async (captchaOptions: { reject([errorCodes.join(','), id]); }, 'unsupported-callback': function () { - reject(['This browser is unsupported by the CAPTCHA.', id]); + reject(['This browser is not supported by the CAPTCHA.', id]); return true; }, }); From 8ed3b0a7cfebd276b5010962181c285742928322 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 15 Apr 2024 17:42:33 +0300 Subject: [PATCH 3/4] fix(clerk-js): Add comment to explain the captcha fallback mechanism --- packages/clerk-js/src/utils/captcha.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/clerk-js/src/utils/captcha.ts b/packages/clerk-js/src/utils/captcha.ts index bfc74330c3..888e8d662e 100644 --- a/packages/clerk-js/src/utils/captcha.ts +++ b/packages/clerk-js/src/utils/captcha.ts @@ -84,6 +84,13 @@ export async function loadCaptcha(url: string) { return window.turnstile; } +/* + * How this function works: + * The widgetType is either 'invisible' or 'smart'. + * - If the widgetType is 'invisible', the captcha widget is rendered in a hidden div at the bottom of the body. + * - If the widgetType is 'smart', the captcha widget is rendered in a div with the id 'clerk-captcha'. If the div does + * not exist, the invisibleSiteKey is used as a fallback and the widget is rendered in a hidden div at the bottom of the body. + */ export const getCaptchaToken = async (captchaOptions: { siteKey: string; scriptUrl: string; From c6d2c502275d24f692f621324804362dc3cf8ca0 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Tue, 16 Apr 2024 11:28:34 +0300 Subject: [PATCH 4/4] fix(clerk-js): Use the invisible key when DOM node is missing --- packages/clerk-js/src/utils/captcha.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/utils/captcha.ts b/packages/clerk-js/src/utils/captcha.ts index 888e8d662e..c6b77624bf 100644 --- a/packages/clerk-js/src/utils/captcha.ts +++ b/packages/clerk-js/src/utils/captcha.ts @@ -97,10 +97,11 @@ export const getCaptchaToken = async (captchaOptions: { widgetType: CaptchaWidgetType; invisibleSiteKey: string; }) => { - const { siteKey: sitekey, scriptUrl, widgetType } = captchaOptions; + const { siteKey, scriptUrl, widgetType, invisibleSiteKey } = captchaOptions; let captchaToken = '', id = ''; let invisibleWidget = !widgetType || widgetType === 'invisible'; + let turnstileSiteKey = siteKey; let widgetDiv: HTMLElement | null = null; @@ -122,6 +123,7 @@ export const getCaptchaToken = async (captchaOptions: { console.error('Captcha DOM element not found. Using invisible captcha widget.'); widgetDiv = createInvisibleDOMElement(); invisibleWidget = true; + turnstileSiteKey = invisibleSiteKey; } } @@ -133,7 +135,7 @@ export const getCaptchaToken = async (captchaOptions: { return new Promise((resolve, reject) => { try { const id = captcha.render(invisibleWidget ? `.${CAPTCHA_INVISIBLE_CLASSNAME}` : `#${CAPTCHA_ELEMENT_ID}`, { - sitekey, + sitekey: turnstileSiteKey, appearance: 'interaction-only', retry: 'never', 'refresh-expired': 'auto',