diff --git a/src/web-auth/captcha.js b/src/web-auth/captcha.js index de46558f..c23537ab 100644 --- a/src/web-auth/captcha.js +++ b/src/web-auth/captcha.js @@ -11,6 +11,7 @@ var HCAPTCHA_PROVIDER = 'hcaptcha'; var FRIENDLY_CAPTCHA_PROVIDER = 'friendly_captcha'; var ARKOSE_PROVIDER = 'arkose'; var AUTH0_PROVIDER = 'auth0'; +var AUTH0_V2_CAPTCHA_PROVIDER = 'auth0_v2'; var MAX_RETRY = 3; var defaults = { @@ -50,6 +51,9 @@ var defaults = { arkose: function () { return '
'; }, + auth0_v2: function () { + return '
'; + }, error: function () { return '
Error getting the bot detection challenge. Please contact the system administrator.
'; } @@ -78,6 +82,8 @@ function globalForCaptchaProvider(provider) { return window.friendlyChallenge; case ARKOSE_PROVIDER: return window.arkose; + case AUTH0_V2_CAPTCHA_PROVIDER: + return window.turnstile; /* istanbul ignore next */ default: throw new Error('Unknown captcha provider'); @@ -120,6 +126,11 @@ function scriptForCaptchaProvider( siteKey + '/api.js' ); + case AUTH0_V2_CAPTCHA_PROVIDER: + return ( + 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=' + + callback + ); /* istanbul ignore next */ default: throw new Error('Unknown captcha provider'); @@ -238,6 +249,9 @@ function handleCaptchaProvider(element, options, challenge) { case ARKOSE_PROVIDER: captchaClass = '.arkose'; break; + case AUTH0_V2_CAPTCHA_PROVIDER: + captchaClass = '.auth0_v2'; + break; } var captchaDiv = element.querySelector(captchaClass); @@ -285,7 +299,7 @@ function handleCaptchaProvider(element, options, challenge) { } }); } else { - widgetId = global.render(captchaDiv, { + var renderParams = { callback: setValue, 'expired-callback': function () { setValue(); @@ -294,7 +308,12 @@ function handleCaptchaProvider(element, options, challenge) { setValue(); }, sitekey: challenge.siteKey - }); + }; + if (challenge.provider === AUTH0_V2_CAPTCHA_PROVIDER) { + renderParams.language = options.lang; + renderParams.theme = 'light'; + } + widgetId = global.render(captchaDiv, renderParams); element.setAttribute('data-wid', widgetId); } }, @@ -316,6 +335,7 @@ function handleCaptchaProvider(element, options, challenge) { * @param {Function} [options.templates.hcaptcha] template function receiving the challenge and returning a string * @param {Function} [options.templates.friendly_captcha] template function receiving the challenge and returning a string * @param {Function} [options.templates.arkose] template function receiving the challenge and returning a string + * @param {Function} [options.templates.auth0_v2] template function receiving the challenge and returning a string * @param {Function} [options.templates.error] template function returning a custom error message when the challenge could not be fetched, receives the error as first argument * @param {String} [options.lang=en] the ISO code of the language for recaptcha * @param {Function} [callback] An optional callback called after captcha is loaded @@ -343,7 +363,8 @@ function render(auth0Client, element, options, callback) { challenge.provider === RECAPTCHA_ENTERPRISE_PROVIDER || challenge.provider === HCAPTCHA_PROVIDER || challenge.provider === FRIENDLY_CAPTCHA_PROVIDER || - challenge.provider === ARKOSE_PROVIDER + challenge.provider === ARKOSE_PROVIDER || + challenge.provider === AUTH0_V2_CAPTCHA_PROVIDER ) { handleCaptchaProvider(element, options, challenge); } @@ -390,6 +411,7 @@ function render(auth0Client, element, options, callback) { * @param {Function} [options.templates.hcaptcha] template function receiving the challenge and returning a string * @param {Function} [options.templates.friendly_captcha] template function receiving the challenge and returning a string * @param {Function} [options.templates.arkose] template function receiving the challenge and returning a string + * @param {Function} [options.templates.auth0_v2] template function receiving the challenge and returning a string * @param {Function} [options.templates.error] template function returning a custom error message when the challenge could not be fetched, receives the error as first argument * @param {String} [options.lang=en] the ISO code of the language for recaptcha * @param {Function} [callback] An optional callback called after captcha is loaded @@ -417,7 +439,8 @@ function renderPasswordless(auth0Client, element, options, callback) { challenge.provider === RECAPTCHA_ENTERPRISE_PROVIDER || challenge.provider === HCAPTCHA_PROVIDER || challenge.provider === FRIENDLY_CAPTCHA_PROVIDER || - challenge.provider === ARKOSE_PROVIDER + challenge.provider === ARKOSE_PROVIDER || + challenge.provider === AUTH0_V2_CAPTCHA_PROVIDER ) { handleCaptchaProvider(element, options, challenge); } diff --git a/src/web-auth/index.js b/src/web-auth/index.js index 44c4350a..4733bddd 100644 --- a/src/web-auth/index.js +++ b/src/web-auth/index.js @@ -1151,6 +1151,7 @@ WebAuth.prototype.passwordlessVerify = function (options, cb) { * @param {Function} [options.templates.hcaptcha] template function receiving the challenge and returning a string * @param {Function} [options.templates.friendly_captcha] template function receiving the challenge and returning a string * @param {Function} [options.templates.arkose] template function receiving the challenge and returning a string + * @param {Function} [options.templates.auth0_v2] template function receiving the challenge and returning a string * @param {Function} [options.templates.error] template function returning a custom error message when the challenge could not be fetched, receives the error as first argument * @param {String} [options.lang=en] the ISO code of the language for the captcha provider * @param {captchaLoadedCallback} [callback] An optional callback called after captcha is loaded @@ -1175,6 +1176,7 @@ WebAuth.prototype.renderCaptcha = function (element, options, callback) { * @param {Function} [options.templates.hcaptcha] template function receiving the challenge and returning a string * @param {Function} [options.templates.friendly_captcha] template function receiving the challenge and returning a string * @param {Function} [options.templates.arkose] template function receiving the challenge and returning a string + * @param {Function} [options.templates.auth0_v2] template function receiving the challenge and returning a string * @param {Function} [options.templates.error] template function returning a custom error message when the challenge could not be fetched, receives the error as first argument * @param {String} [options.lang=en] the ISO code of the language for the captcha provider * @param {captchaLoadedCallback} [callback] An optional callback called after captcha is loaded diff --git a/test/web-auth/captcha.test.js b/test/web-auth/captcha.test.js index ddcb9fd0..78b19422 100644 --- a/test/web-auth/captcha.test.js +++ b/test/web-auth/captcha.test.js @@ -192,12 +192,14 @@ describe('captcha rendering', function () { const HCAPTCHA_PROVIDER = 'hcaptcha'; const FRIENDLY_CAPTCHA_PROVIDER = 'friendly_captcha'; const ARKOSE_PROVIDER = 'arkose'; + const AUTH0_V2_CAPTCHA_PROVIDER = 'auth0_v2'; [ RECAPTCHA_V2_PROVIDER, RECAPTCHA_ENTERPRISE_PROVIDER, HCAPTCHA_PROVIDER, - FRIENDLY_CAPTCHA_PROVIDER + FRIENDLY_CAPTCHA_PROVIDER, + AUTH0_V2_CAPTCHA_PROVIDER ].forEach(provider => { const getScript = () => { switch (provider) { @@ -209,19 +211,19 @@ describe('captcha rendering', function () { return 'enterprise.js'; case FRIENDLY_CAPTCHA_PROVIDER: return 'widget.min.js'; + case AUTH0_V2_CAPTCHA_PROVIDER: + return 'api.js'; } }; const getHostname = () => { - switch (provider) { - case RECAPTCHA_V2_PROVIDER: - return 'recaptcha.net'; - case RECAPTCHA_ENTERPRISE_PROVIDER: - return 'recaptcha.net'; - case HCAPTCHA_PROVIDER: - return 'hcaptcha.com'; - case FRIENDLY_CAPTCHA_PROVIDER: - return 'jsdelivr.net'; + const hosts = { + [RECAPTCHA_V2_PROVIDER]: 'recaptcha.net', + [RECAPTCHA_ENTERPRISE_PROVIDER]: 'recaptcha.net', + [HCAPTCHA_PROVIDER]: 'hcaptcha.com', + [FRIENDLY_CAPTCHA_PROVIDER]: 'jsdelivr.net', + [AUTH0_V2_CAPTCHA_PROVIDER]: 'cloudflare.com' } + return hosts[provider]; }; const getSubdomain = () => { switch (provider) { @@ -233,6 +235,8 @@ describe('captcha rendering', function () { return 'js'; case FRIENDLY_CAPTCHA_PROVIDER: return 'cdn'; + case AUTH0_V2_CAPTCHA_PROVIDER: + return 'challenges'; } }; const getPath = () => { @@ -245,6 +249,8 @@ describe('captcha rendering', function () { return '1'; case FRIENDLY_CAPTCHA_PROVIDER: return 'npm/friendly-challenge@0.9.12'; + case AUTH0_V2_CAPTCHA_PROVIDER: + return 'turnstile/v0'; } }; const setMockGlobal = mock => { @@ -261,6 +267,9 @@ describe('captcha rendering', function () { case FRIENDLY_CAPTCHA_PROVIDER: window.friendlyChallenge = mock; break; + case AUTH0_V2_CAPTCHA_PROVIDER: + window.turnstile = mock; + break; } }; describe(`when challenge is required and provider is ${provider}`, function () { @@ -303,8 +312,13 @@ describe('captcha rendering', function () { `${getSubdomain()}.${getHostname()}` ); expect(scriptUrl.pathname).to.equal(`/${getPath()}/${getScript()}`); - if (provider !== FRIENDLY_CAPTCHA_PROVIDER) { + if ( + provider !== FRIENDLY_CAPTCHA_PROVIDER && + provider !== AUTH0_V2_CAPTCHA_PROVIDER + ) { expect(scriptUrl.query.hl).to.equal('en'); + } + if (provider !== FRIENDLY_CAPTCHA_PROVIDER) { expect(scriptUrl.query).to.have.property('onload'); } }); @@ -748,12 +762,14 @@ describe('passwordless captcha rendering', function () { const HCAPTCHA_PROVIDER = 'hcaptcha'; const FRIENDLY_CAPTCHA_PROVIDER = 'friendly_captcha'; const ARKOSE_PROVIDER = 'arkose'; + const AUTH0_V2_CAPTCHA_PROVIDER = 'auth0_v2'; [ RECAPTCHA_V2_PROVIDER, RECAPTCHA_ENTERPRISE_PROVIDER, HCAPTCHA_PROVIDER, - FRIENDLY_CAPTCHA_PROVIDER + FRIENDLY_CAPTCHA_PROVIDER, + AUTH0_V2_CAPTCHA_PROVIDER ].forEach(provider => { const getScript = () => { switch (provider) { @@ -765,19 +781,19 @@ describe('passwordless captcha rendering', function () { return 'enterprise.js'; case FRIENDLY_CAPTCHA_PROVIDER: return 'widget.min.js'; + case AUTH0_V2_CAPTCHA_PROVIDER: + return 'api.js'; } }; const getHostname = () => { - switch (provider) { - case RECAPTCHA_V2_PROVIDER: - return 'recaptcha.net'; - case RECAPTCHA_ENTERPRISE_PROVIDER: - return 'recaptcha.net'; - case HCAPTCHA_PROVIDER: - return 'hcaptcha.com'; - case FRIENDLY_CAPTCHA_PROVIDER: - return 'jsdelivr.net'; + const hosts = { + [RECAPTCHA_V2_PROVIDER]: 'recaptcha.net', + [RECAPTCHA_ENTERPRISE_PROVIDER]: 'recaptcha.net', + [HCAPTCHA_PROVIDER]: 'hcaptcha.com', + [FRIENDLY_CAPTCHA_PROVIDER]: 'jsdelivr.net', + [AUTH0_V2_CAPTCHA_PROVIDER]: 'cloudflare.com' } + return hosts[provider]; }; const getSubdomain = () => { switch (provider) { @@ -789,6 +805,8 @@ describe('passwordless captcha rendering', function () { return 'js'; case FRIENDLY_CAPTCHA_PROVIDER: return 'cdn'; + case AUTH0_V2_CAPTCHA_PROVIDER: + return 'challenges'; } }; const getPath = () => { @@ -801,6 +819,8 @@ describe('passwordless captcha rendering', function () { return '1'; case FRIENDLY_CAPTCHA_PROVIDER: return 'npm/friendly-challenge@0.9.12'; + case AUTH0_V2_CAPTCHA_PROVIDER: + return 'turnstile/v0'; } }; const setMockGlobal = mock => { @@ -817,6 +837,9 @@ describe('passwordless captcha rendering', function () { case FRIENDLY_CAPTCHA_PROVIDER: window.friendlyChallenge = mock; break; + case AUTH0_V2_CAPTCHA_PROVIDER: + window.turnstile = mock; + break; } }; describe(`when challenge is required and provider is ${provider}`, function () { @@ -861,8 +884,13 @@ describe('passwordless captcha rendering', function () { `${getSubdomain()}.${getHostname()}` ); expect(scriptUrl.pathname).to.equal(`/${getPath()}/${getScript()}`); - if (provider !== FRIENDLY_CAPTCHA_PROVIDER) { + if ( + provider !== FRIENDLY_CAPTCHA_PROVIDER && + provider !== AUTH0_V2_CAPTCHA_PROVIDER + ) { expect(scriptUrl.query.hl).to.equal('en'); + } + if (provider !== FRIENDLY_CAPTCHA_PROVIDER) { expect(scriptUrl.query).to.have.property('onload'); } });