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()); } }); }