diff --git a/css/index.styl b/css/index.styl
index 07eb0902b..929ae183e 100644
--- a/css/index.styl
+++ b/css/index.styl
@@ -1414,6 +1414,18 @@ loadingSize = 30px
transform-origin: 0px 0px;
position: relative;
+.auth0-lock-auth0-v2-block
+ border-radius: 4px;
+ height: 65px;
+
+ &.auth0-lock-auth0-v2-block-error
+ border: 1px solid red;
+
+ .auth0-lock-auth0-v2
+ transform: scale(0.855);
+ transform-origin: 0px 0px;
+ position: relative;
+
.auth0-lock-friendly-captcha-block
border-radius: 4px;
border: 1px solid #eee;
diff --git a/src/__tests__/field/captcha.test.jsx b/src/__tests__/field/captcha.test.jsx
index d94a92f1e..274ffad14 100644
--- a/src/__tests__/field/captcha.test.jsx
+++ b/src/__tests__/field/captcha.test.jsx
@@ -100,6 +100,28 @@ describe('CaptchaPane', () => {
});
});
+ describe('auth0_v2', () => {
+ let wrapper;
+ beforeAll(() => {
+ const lockMock = createLockMock({
+ provider: 'auth0_v2',
+ siteKey: 'mySiteKey'
+ });
+ const i8nMock = createI18nMock();
+ const onReloadMock = jest.fn();
+
+ wrapper = mount();
+ });
+
+ it('should render ThirdPartyCaptcha if provider is auth0_v2', () => {
+ expect(wrapper.find(ThirdPartyCaptcha)).toHaveLength(1);
+ });
+
+ it('should pass the sitekey', () => {
+ expect(wrapper.find(ThirdPartyCaptcha).props().sitekey).toBe('mySiteKey');
+ });
+ });
+
describe('recaptcha enterprise', () => {
let wrapper;
beforeAll(() => {
diff --git a/src/__tests__/field/captcha/__snapshots__/auth0_v2.test.jsx.snap b/src/__tests__/field/captcha/__snapshots__/auth0_v2.test.jsx.snap
new file mode 100644
index 000000000..71c7e969c
--- /dev/null
+++ b/src/__tests__/field/captcha/__snapshots__/auth0_v2.test.jsx.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Auth0 V2 should match the snapshot 1`] = `
+
+`;
diff --git a/src/__tests__/field/captcha/auth0_v2.test.jsx b/src/__tests__/field/captcha/auth0_v2.test.jsx
new file mode 100644
index 000000000..98f7a51d5
--- /dev/null
+++ b/src/__tests__/field/captcha/auth0_v2.test.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { expectComponent } from 'testUtils';
+import { ThirdPartyCaptcha } from '../../../field/captcha/third_party_captcha';
+
+describe('Auth0 V2', () => {
+ const component = ;
+
+ it('should match the snapshot', () => {
+ expectComponent(component).toMatchSnapshot();
+ });
+
+ it('injects the script', () => {
+ const script = [...window.document.querySelectorAll('script')].find(s =>
+ s.src.startsWith(
+ 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload='
+ )
+ );
+ expect(script).not.toBeUndefined();
+ });
+});
diff --git a/src/__tests__/field/captcha/third_party_captcha.test.jsx b/src/__tests__/field/captcha/third_party_captcha.test.jsx
new file mode 100644
index 000000000..31adbdbe3
--- /dev/null
+++ b/src/__tests__/field/captcha/third_party_captcha.test.jsx
@@ -0,0 +1,244 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import I from 'immutable';
+import * as l from '../../../core/index';
+import { ThirdPartyCaptcha } from '../../../field/captcha/third_party_captcha';
+
+const createLockMock = ({
+ provider = 'auth0',
+ required = true,
+ siteKey = '',
+ clientSubdomain = ''
+} = {}) =>
+ I.fromJS({
+ id: '__lock-id__',
+ core: {
+ captcha: { provider, siteKey, clientSubdomain, required: required }
+ }
+ });
+
+describe('ThirdPartyCaptcha', () => {
+ let prevWindow;
+ beforeAll(() => {
+ prevWindow = global.window;
+ global.window.grecaptcha = {
+ render: jest.fn(),
+ enterprise: {
+ render: jest.fn()
+ }
+ };
+ global.window.hcaptcha = {
+ render: jest.fn()
+ };
+ global.window.friendlyChallenge = {
+ WidgetInstance: jest.fn().mockImplementation((...args) => {
+ return jest.fn(...args);
+ })
+ };
+ global.window.turnstile = {
+ render: jest.fn()
+ };
+ });
+ afterAll(() => {
+ global.window = prevWindow;
+ });
+ describe('recaptchav2', () => {
+ let wrapper;
+ beforeAll(() => {
+ const lockMock = createLockMock({
+ provider: 'recaptcha_v2',
+ siteKey: 'mySiteKey'
+ });
+
+ const captcha = l.captcha(lockMock);
+ wrapper = mount(
+
+ ).instance();
+ act(() => {
+ const injectCaptchaScriptSpy = jest.spyOn(wrapper, 'injectCaptchaScript');
+
+ wrapper.componentDidMount();
+
+ injectCaptchaScriptSpy.mock.calls[0][0]();
+ });
+ });
+
+ it('should call render with the correct renderParams', () => {
+ const renderParams = global.window.grecaptcha.render.mock.calls[0][1];
+
+ expect(renderParams).toEqual({
+ sitekey: 'mySiteKey',
+ callback: expect.any(Function),
+ 'expired-callback': expect.any(Function),
+ 'error-callback': expect.any(Function)
+ });
+ });
+ });
+
+ describe('friendly captcha', () => {
+ let wrapper;
+ beforeAll(() => {
+ const lockMock = createLockMock({
+ provider: 'friendly_captcha',
+ siteKey: 'mySiteKey'
+ });
+
+ const captcha = l.captcha(lockMock);
+ wrapper = mount(
+
+ ).instance();
+ act(() => {
+ const injectCaptchaScriptSpy = jest.spyOn(wrapper, 'injectCaptchaScript');
+
+ wrapper.componentDidMount();
+ jest.spyOn(global.window.friendlyChallenge, 'WidgetInstance');
+
+ injectCaptchaScriptSpy.mock.calls[0][0]();
+ });
+ });
+
+ it('should call WidgetInstance constructor with the correct renderParams', () => {
+ const renderParams = global.window.friendlyChallenge.WidgetInstance.mock.calls[0][1];
+ expect(renderParams).toEqual({
+ sitekey: 'mySiteKey',
+ doneCallback: expect.any(Function),
+ errorCallback: expect.any(Function),
+ language: 'en'
+ });
+ });
+ });
+
+ describe('hcaptcha', () => {
+ let wrapper;
+ beforeAll(() => {
+ const lockMock = createLockMock({
+ provider: 'hcaptcha',
+ siteKey: 'mySiteKey'
+ });
+
+ const captcha = l.captcha(lockMock);
+ wrapper = mount(
+
+ ).instance();
+ act(() => {
+ const injectCaptchaScriptSpy = jest.spyOn(wrapper, 'injectCaptchaScript');
+
+ wrapper.componentDidMount();
+
+ injectCaptchaScriptSpy.mock.calls[0][0]();
+ });
+ });
+
+ it('should call render with the correct renderParams', () => {
+ const renderParams = global.window.hcaptcha.render.mock.calls[0][1];
+ expect(renderParams).toEqual({
+ sitekey: 'mySiteKey',
+ callback: expect.any(Function),
+ 'expired-callback': expect.any(Function),
+ 'error-callback': expect.any(Function)
+ });
+ });
+ });
+
+ describe('auth0_v2', () => {
+ let wrapper;
+ beforeAll(() => {
+ const lockMock = createLockMock({
+ provider: 'auth0_v2',
+ siteKey: 'mySiteKey'
+ });
+
+ const captcha = l.captcha(lockMock);
+ wrapper = mount(
+
+ ).instance();
+ act(() => {
+ const injectCaptchaScriptSpy = jest.spyOn(wrapper, 'injectCaptchaScript');
+
+ wrapper.componentDidMount();
+
+ injectCaptchaScriptSpy.mock.calls[0][0]();
+ });
+ });
+
+ it('should call render with the correct renderParams', () => {
+ const renderParams = global.window.turnstile.render.mock.calls[0][1];
+ expect(renderParams).toEqual({
+ sitekey: 'mySiteKey',
+ callback: expect.any(Function),
+ 'expired-callback': expect.any(Function),
+ 'error-callback': expect.any(Function),
+ language: 'en',
+ theme: 'light'
+ });
+ });
+ });
+
+ describe('recaptcha enterprise', () => {
+ let wrapper;
+ beforeAll(() => {
+ const lockMock = createLockMock({
+ provider: 'recaptcha_enterprise',
+ siteKey: 'mySiteKey'
+ });
+
+ const captcha = l.captcha(lockMock);
+ wrapper = mount(
+
+ ).instance();
+ act(() => {
+ const injectCaptchaScriptSpy = jest.spyOn(wrapper, 'injectCaptchaScript');
+
+ wrapper.componentDidMount();
+
+ injectCaptchaScriptSpy.mock.calls[0][0]();
+ });
+ });
+
+ it('should call render with the correct renderParams', () => {
+ const renderParams = global.window.grecaptcha.enterprise.render.mock.calls[0][1];
+ expect(renderParams).toEqual({
+ sitekey: 'mySiteKey',
+ callback: expect.any(Function),
+ 'expired-callback': expect.any(Function),
+ 'error-callback': expect.any(Function)
+ });
+ });
+ });
+});
diff --git a/src/connection/captcha.js b/src/connection/captcha.js
index 8f26a8b86..f166ceb1a 100644
--- a/src/connection/captcha.js
+++ b/src/connection/captcha.js
@@ -18,6 +18,7 @@ export function showMissingCaptcha(m, id, isPasswordless = false) {
captchaConfig.get('provider') === 'recaptcha_v2' ||
captchaConfig.get('provider') === 'recaptcha_enterprise' ||
captchaConfig.get('provider') === 'hcaptcha' ||
+ captchaConfig.get('provider') === 'auth0_v2' ||
captchaConfig.get('provider') === 'friendly_captcha'
) ? 'invalid_recaptcha' : 'invalid_captcha';
diff --git a/src/connection/passwordless/actions.js b/src/connection/passwordless/actions.js
index 0d617e741..9c25598ed 100644
--- a/src/connection/passwordless/actions.js
+++ b/src/connection/passwordless/actions.js
@@ -34,6 +34,7 @@ function getErrorMessage(m, id, error) {
captchaConfig.get('provider') === 'recaptcha_v2' ||
captchaConfig.get('provider') === 'recaptcha_enterprise' ||
captchaConfig.get('provider') === 'hcaptcha' ||
+ captchaConfig.get('provider') === 'auth0_v2' ||
captchaConfig.get('provider') === 'friendly_captcha'
) ? 'invalid_recaptcha' : 'invalid_captcha';
}
diff --git a/src/core/index.js b/src/core/index.js
index 1b38a6477..03c0f5164 100644
--- a/src/core/index.js
+++ b/src/core/index.js
@@ -584,6 +584,7 @@ export function loginErrorMessage(m, error, type) {
currentCaptcha.get('provider') === 'recaptcha_v2' ||
currentCaptcha.get('provider') === 'recaptcha_enterprise' ||
currentCaptcha.get('provider') === 'hcaptcha' ||
+ currentCaptcha.get('provider') === 'auth0_v2' ||
captchaConfig.get('provider') === 'friendly_captcha'
)) {
code = 'invalid_recaptcha';
diff --git a/src/field/captcha/third_party_captcha.jsx b/src/field/captcha/third_party_captcha.jsx
index d0874e7dd..6a5da9676 100644
--- a/src/field/captcha/third_party_captcha.jsx
+++ b/src/field/captcha/third_party_captcha.jsx
@@ -9,6 +9,7 @@ const RECAPTCHA_ENTERPRISE_PROVIDER = 'recaptcha_enterprise';
const HCAPTCHA_PROVIDER = 'hcaptcha';
const FRIENDLY_CAPTCHA_PROVIDER = 'friendly_captcha';
const ARKOSE_PROVIDER = 'arkose';
+const AUTH0_V2_CAPTCHA_PROVIDER = 'auth0_v2';
const TIMEOUT_MS = 500;
const MAX_RETRY = 3;
@@ -17,7 +18,8 @@ export const isThirdPartyCaptcha = provider =>
|| provider === RECAPTCHA_V2_PROVIDER
|| provider === HCAPTCHA_PROVIDER
|| provider === FRIENDLY_CAPTCHA_PROVIDER
- || provider === ARKOSE_PROVIDER;
+ || provider === ARKOSE_PROVIDER
+ || provider === AUTH0_V2_CAPTCHA_PROVIDER;
const getCaptchaProvider = provider => {
switch (provider) {
@@ -31,6 +33,8 @@ const getCaptchaProvider = provider => {
return window.friendlyChallenge;
case ARKOSE_PROVIDER:
return window.arkose;
+ case AUTH0_V2_CAPTCHA_PROVIDER:
+ return window.turnstile;
}
};
@@ -46,6 +50,8 @@ const scriptForProvider = (provider, lang, callback, clientSubdomain, siteKey) =
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}`;
}
};
@@ -61,6 +67,8 @@ const providerDomPrefix = (provider) => {
return 'friendly-captcha';
case ARKOSE_PROVIDER:
return 'arkose';
+ case AUTH0_V2_CAPTCHA_PROVIDER:
+ return 'auth0-v2';
}
};
@@ -115,9 +123,37 @@ export class ThirdPartyCaptcha extends React.Component {
this.props.onChange(value);
this.props.onErrored();
});
- };
+ };
}
+ getRenderParams() {
+
+
+ if (this.props.provider === FRIENDLY_CAPTCHA_PROVIDER) {
+ return {
+ sitekey: this.props.sitekey,
+ language: this.props.hl,
+ doneCallback: this.changeHandler,
+ errorCallback: this.erroredHandler
+ };
+ }
+ let renderParams = {
+ sitekey: this.props.sitekey,
+ callback: this.changeHandler,
+ 'expired-callback': this.expiredHandler,
+ 'error-callback': this.erroredHandler
+ };
+
+ if (this.props.provider === AUTH0_V2_CAPTCHA_PROVIDER) {
+ renderParams = {
+ ...renderParams,
+ language: this.props.hl,
+ theme: 'light'
+ };
+ }
+ return renderParams;
+ }
+
injectCaptchaScript(callback = noop) {
const { provider, hl, clientSubdomain, sitekey } = this.props;
const callbackName = `${providerDomPrefix(provider)}Callback_${Math.floor(Math.random() * 1000001)}`;
@@ -192,20 +228,10 @@ export class ThirdPartyCaptcha extends React.Component {
}
});
} else if (this.props.provider === FRIENDLY_CAPTCHA_PROVIDER) {
- this.widgetInstance = new provider.WidgetInstance(this.ref.current, {
- sitekey: this.props.sitekey,
- language: this.props.hl,
- doneCallback: this.changeHandler,
- errorCallback: this.erroredHandler,
- });
+ this.widgetInstance = new provider.WidgetInstance(this.ref.current, this.getRenderParams());
} else {
// if this is enterprise then we change this to window.grecaptcha.enterprise.render
- this.widgetId = provider.render(this.ref.current, {
- callback: this.changeHandler,
- 'expired-callback': this.expiredHandler,
- 'error-callback': this.erroredHandler,
- sitekey: this.props.sitekey
- });
+ this.widgetId = provider.render(this.ref.current, this.getRenderParams());
}
});
}