auth0-js
Version:
Auth0 headless browser sdk
472 lines (447 loc) • 15.1 kB
JavaScript
// eslint-disable-next-line no-unused-vars
import Authentication from '../authentication';
import object from '../helper/object';
var noop = function () {};
var captchaSolved = noop;
var done = noop;
var captchaReloaded = false;
var Flow = {
DEFAULT: 'default',
SIGNUP: 'signup',
PASSWORDLESS: 'passwordless',
PASSWORD_RESET: 'password_reset'
};
var RECAPTCHA_V2_PROVIDER = 'recaptcha_v2';
var RECAPTCHA_ENTERPRISE_PROVIDER = 'recaptcha_enterprise';
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 = {
lang: 'en',
appearance: 'always',
successCallback: noop,
templates: {
auth0: function (challenge) {
var message =
challenge.type === 'code'
? 'Enter the code shown above'
: 'Solve the formula shown above';
return (
'<div class="captcha-challenge">\n' +
' <img src="" />\n' +
' <button type="button" class="captcha-reload">↺</button>\n' +
'</div>\n' +
'<input type="text" name="captcha"\n' +
' class="form-control captcha-control"\n' +
' placeholder="' +
message +
'" />'
);
},
recaptcha_v2: function () {
return '<div class="recaptcha" ></div><input type="hidden" name="captcha" />';
},
recaptcha_enterprise: function () {
return '<div class="recaptcha" ></div><input type="hidden" name="captcha" />';
},
hcaptcha: function () {
return '<div class="hcaptcha" ></div><input type="hidden" name="captcha" />';
},
friendly_captcha: function () {
return '<div class="friendly-captcha" ></div><input type="hidden" name="captcha" />';
},
arkose: function () {
return '<div class="arkose" ></div><input type="hidden" name="captcha" />';
},
auth0_v2: function () {
return '<div class="auth0_v2" ></div><input type="hidden" name="captcha" />';
},
error: function () {
return '<div class="error" style="color: red;">Error getting the bot detection challenge. Please contact the system administrator.</div>';
}
}
};
function escapeAttr(str) {
return String(str || '').replace(/&/g, '&').replace(/"/g, '"');
}
function handleAuth0Provider(element, options, challenge, load) {
var safeChallenge = Object.assign({}, challenge, {
image: escapeAttr(challenge.image)
});
element.innerHTML = options.templates[challenge.provider](safeChallenge);
var img = element.querySelector('.captcha-challenge img');
if (img) {
img.setAttribute('src', challenge.image || '');
}
var reloadBtn = element.querySelector('.captcha-reload');
if (reloadBtn) {
reloadBtn.addEventListener('click', function (e) {
e.preventDefault();
load();
});
}
}
function globalForCaptchaProvider(provider) {
switch (provider) {
case RECAPTCHA_V2_PROVIDER:
return window.grecaptcha;
case RECAPTCHA_ENTERPRISE_PROVIDER:
return window.grecaptcha.enterprise;
case HCAPTCHA_PROVIDER:
return window.hcaptcha;
case FRIENDLY_CAPTCHA_PROVIDER:
return window.friendlyChallenge;
case ARKOSE_PROVIDER:
return window.arkose;
case AUTH0_V2_CAPTCHA_PROVIDER:
return window.turnstile;
default:
throw new Error('Unknown captcha provider');
}
}
function scriptForCaptchaProvider(
provider,
lang,
callback,
clientSubdomain,
siteKey
) {
switch (provider) {
case RECAPTCHA_V2_PROVIDER:
return (
'https://www.recaptcha.net/recaptcha/api.js?hl=' +
lang +
'&onload=' +
callback
);
case RECAPTCHA_ENTERPRISE_PROVIDER:
return (
'https://www.recaptcha.net/recaptcha/enterprise.js?render=explicit&hl=' +
lang +
'&onload=' +
callback
);
case HCAPTCHA_PROVIDER:
return (
'https://js.hcaptcha.com/1/api.js?hl=' + lang + '&onload=' + callback
);
case FRIENDLY_CAPTCHA_PROVIDER:
return 'https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.12/widget.min.js';
case ARKOSE_PROVIDER:
return (
'https://' +
clientSubdomain +
'.arkoselabs.com/v2/' +
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');
}
}
function loadScript(url, attributes) {
var script = window.document.createElement('script');
for (var attr in attributes) {
if (attr.startsWith('data-')) {
script.dataset[attr.replace('data-', '')] = attributes[attr];
} else {
script[attr] = attributes[attr];
}
}
script.src = url;
window.document.body.appendChild(script);
}
function removeScript(url) {
var scripts = window.document.querySelectorAll('script[src="' + url + '"]');
scripts.forEach(function (script) {
script.remove();
});
}
function injectCaptchaScript(opts, callback, setValue) {
var callbackName =
opts.provider + 'Callback_' + Math.floor(Math.random() * 1000001);
var attributes = {
async: true,
defer: true
};
var scriptSrc = scriptForCaptchaProvider(
opts.provider,
opts.lang,
callbackName,
opts.clientSubdomain,
opts.siteKey
);
if (
opts.provider === ARKOSE_PROVIDER ||
opts.provider === AUTH0_V2_CAPTCHA_PROVIDER
) {
var retryCount = 0;
attributes['data-callback'] = callbackName;
attributes['onerror'] = function () {
if (retryCount < MAX_RETRY) {
removeScript(scriptSrc);
loadScript(scriptSrc, attributes);
retryCount++;
return;
}
removeScript(scriptSrc);
done(new Error(opts.provider + ' failed to load'));
// Optimzation to tell auth0 to fail open if Arkose/auth0_v2 is configured to fail open
setValue('BYPASS_CAPTCHA');
};
window[callbackName] = function (arkose) {
window.arkose = arkose;
callback(arkose);
};
} else {
window[callbackName] = function () {
delete window[callbackName];
callback();
};
if (opts.provider === FRIENDLY_CAPTCHA_PROVIDER) {
attributes['onload'] = window[callbackName];
}
}
loadScript(scriptSrc, attributes);
}
function handleCaptchaProvider(element, options, challenge) {
var widgetId =
element.hasAttribute('data-wid') && element.getAttribute('data-wid');
function setValue(value) {
var input = element.querySelector('input[name="captcha"]');
input.value = value || '';
}
if (
challenge.provider === FRIENDLY_CAPTCHA_PROVIDER &&
window.auth0FCInstance
) {
setValue();
window.auth0FCInstance.reset();
return;
} else if (
challenge.provider === ARKOSE_PROVIDER &&
globalForCaptchaProvider(challenge.provider)
) {
setValue();
globalForCaptchaProvider(challenge.provider).reset();
return;
} else if (widgetId) {
setValue();
globalForCaptchaProvider(challenge.provider).reset(widgetId);
return;
}
element.innerHTML = options.templates[challenge.provider](challenge);
var captchaClass;
switch (challenge.provider) {
case RECAPTCHA_ENTERPRISE_PROVIDER:
captchaClass = '.recaptcha';
break;
case RECAPTCHA_V2_PROVIDER:
captchaClass = '.recaptcha';
break;
case HCAPTCHA_PROVIDER:
captchaClass = '.hcaptcha';
break;
case FRIENDLY_CAPTCHA_PROVIDER:
captchaClass = '.friendly-captcha';
break;
case ARKOSE_PROVIDER:
captchaClass = '.arkose';
break;
case AUTH0_V2_CAPTCHA_PROVIDER:
captchaClass = '.auth0_v2';
break;
}
var captchaDiv = element.querySelector(captchaClass);
injectCaptchaScript(
{
lang: options.lang,
provider: challenge.provider,
clientSubdomain: challenge.clientSubdomain,
siteKey: challenge.siteKey
},
function (arkose) {
var global = globalForCaptchaProvider(challenge.provider);
if (challenge.provider === ARKOSE_PROVIDER) {
var retryCount = 0;
var arkoseLoaded = false;
arkose.setConfig({
onReady: function () {
if (!arkoseLoaded || captchaReloaded) {
done(null, {
triggerCaptcha: function (solvedCallback) {
arkose.run();
captchaSolved = solvedCallback;
}
});
if (captchaReloaded) {
captchaReloaded = false;
}
arkoseLoaded = true;
}
},
onCompleted: function (response) {
setValue(response.token);
captchaSolved();
},
onError: function (response) {
if (retryCount < MAX_RETRY) {
setValue();
arkose.reset();
// To ensure reset is successful, we need to set a timeout here
setTimeout(function () {
arkose.run();
}, 500);
retryCount++;
} else {
if (!arkoseLoaded) {
done(new Error(response.error.error));
arkoseLoaded = true;
}
// Optimzation to tell auth0 to fail open if Arkose is configured to fail open
setValue('BYPASS_CAPTCHA');
}
}
});
} else if (challenge.provider === FRIENDLY_CAPTCHA_PROVIDER) {
window.auth0FCInstance = new global.WidgetInstance(captchaDiv, {
sitekey: challenge.siteKey,
language: options.lang,
doneCallback: function (solution) {
setValue(solution);
},
errorCallback: function () {
setValue();
}
});
done();
} else {
var renderParams = {
callback: function (token) {
setValue(token);
if (challenge.provider === AUTH0_V2_CAPTCHA_PROVIDER) {
options.successCallback();
}
},
'expired-callback': function () {
setValue();
},
'error-callback': function () {
setValue();
},
sitekey: challenge.siteKey
};
if (challenge.provider === AUTH0_V2_CAPTCHA_PROVIDER) {
retryCount = 0;
renderParams.language = options.lang;
renderParams.appearance = options.appearance;
renderParams.theme = 'light';
renderParams.retry = 'never';
renderParams['response-field'] = false;
renderParams['error-callback'] = function () {
if (retryCount < MAX_RETRY) {
setValue();
globalForCaptchaProvider(challenge.provider).reset(widgetId);
retryCount++;
} else {
setValue('BYPASS_CAPTCHA');
}
return true;
};
}
widgetId = global.render(captchaDiv, renderParams);
element.setAttribute('data-wid', widgetId);
done();
}
},
setValue
);
}
/**
*
* Renders the captcha challenge in the provided element based on the flow.
*
* @param {Authentication} auth0Client The challenge response from the authentication server
* @param {HTMLElement} element The element where the captcha needs to be rendered
* @param {Flow} flow The flow for which the captcha needs to be rendered
* @param {Object} options The configuration options for the captcha
* @param {Object} [options.templates] An object containing templates for each captcha provider
* @param {Function} [options.templates.auth0] template function receiving the challenge and returning a string
* @param {Function} [options.templates.recaptcha_v2] template function receiving the challenge and returning a string
* @param {Function} [options.templates.recaptcha_enterprise] template function receiving the challenge and returning a string
* @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 {String} [options.appearance=always] When the widget is visible: 'always', 'interaction-only'. Only applies to the auth0_v2 provider.
* @param {Function} [options.successCallback] A callback function that is called when the captcha is solved successfully. Only applies to the auth0_v2 provider.
* @param {Function} [callback] An optional callback called after captcha is loaded
* @ignore
*/
function render(auth0Client, flow, element, options, callback) {
options = object.merge(defaults).with(options || {});
function load(loadDone) {
done = loadDone || noop;
function challengeCallback(err, challenge) {
if (err) {
element.innerHTML = options.templates.error(err);
return done(err);
}
if (!challenge?.required) {
element.style.display = 'none';
element.innerHTML = '';
return done();
}
element.style.display = '';
if (challenge.provider === AUTH0_PROVIDER) {
handleAuth0Provider(element, options, challenge, load);
done();
} else if (
challenge.provider === RECAPTCHA_V2_PROVIDER ||
challenge.provider === RECAPTCHA_ENTERPRISE_PROVIDER ||
challenge.provider === HCAPTCHA_PROVIDER ||
challenge.provider === FRIENDLY_CAPTCHA_PROVIDER ||
challenge.provider === ARKOSE_PROVIDER ||
challenge.provider === AUTH0_V2_CAPTCHA_PROVIDER
) {
handleCaptchaProvider(element, options, challenge);
}
}
if (flow === Flow.PASSWORDLESS) {
auth0Client.passwordless.getChallenge(challengeCallback);
} else if (flow === Flow.PASSWORD_RESET) {
auth0Client.dbConnection.getPasswordResetChallenge(challengeCallback);
} else if (flow === Flow.SIGNUP) {
auth0Client.dbConnection.getSignupChallenge(challengeCallback);
} else {
auth0Client.getChallenge(challengeCallback);
}
}
function getValue() {
var captchaInput = element.querySelector('input[name="captcha"]');
if (!captchaInput) {
return;
}
return captchaInput.value;
}
load(callback);
return {
reload: function (reloadDone) {
captchaReloaded = true;
load(reloadDone);
},
getValue: getValue
};
}
export default { render: render, Flow: Flow };