Skip to content

Commit

Permalink
[IAMRISK-2916] Added support for Auth0 v2 captcha provider (#2503)
Browse files Browse the repository at this point in the history
### Changes
Added support for Auth0 v2 (Cloudflare turnstile) captcha provider.

### References
https://auth0team.atlassian.net/browse/IAMRISK-2916

### Testing

* [x] This change adds unit test coverage
* [x] This change adds integration test coverage
* [x] This change has been tested on the latest version of the
platform/language

### Checklist

* [x] I have read the [Auth0 general contribution
guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
* [x] I have read the [Auth0 Code of
Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
* [x] All code quality tools/guidelines have been run/followed
* [x] All relevant assets have been compiled

---------

Co-authored-by: Frederik Prijck <frederik.prijck@auth0.com>
  • Loading branch information
alexkoumarianos-okta and frederikprijck authored Dec 14, 2023
1 parent 003339c commit ea1c9fc
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 14 deletions.
12 changes: 12 additions & 0 deletions css/index.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/field/captcha.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<CaptchaPane lock={lockMock} onReload={onReloadMock} i18n={i8nMock} />);
});

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(() => {
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/field/captcha/__snapshots__/auth0_v2.test.jsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Auth0 V2 should match the snapshot 1`] = `
<div
className="auth0-lock-auth0-v2-block auth0-lock-auth0-v2-block-error"
>
<div
className="auth0-lock-auth0-v2"
/>
</div>
`;
20 changes: 20 additions & 0 deletions src/__tests__/field/captcha/auth0_v2.test.jsx
Original file line number Diff line number Diff line change
@@ -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 = <ThirdPartyCaptcha provider={'auth0_v2'} hl="en" sitekey={'mySiteKey'} />;

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();
});
});
244 changes: 244 additions & 0 deletions src/__tests__/field/captcha/third_party_captcha.test.jsx
Original file line number Diff line number Diff line change
@@ -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(
<ThirdPartyCaptcha
provider={captcha.get('provider')}
sitekey={captcha.get('siteKey')}
clientSubdomain={captcha.get('clientSubdomain')}
hl={'en'}
isValid={true}
value={undefined}
/>
).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(
<ThirdPartyCaptcha
provider={captcha.get('provider')}
sitekey={captcha.get('siteKey')}
clientSubdomain={captcha.get('clientSubdomain')}
hl={'en'}
isValid={true}
value={undefined}
/>
).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(
<ThirdPartyCaptcha
provider={captcha.get('provider')}
sitekey={captcha.get('siteKey')}
clientSubdomain={captcha.get('clientSubdomain')}
hl={'en'}
isValid={true}
value={undefined}
/>
).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(
<ThirdPartyCaptcha
provider={captcha.get('provider')}
sitekey={captcha.get('siteKey')}
clientSubdomain={captcha.get('clientSubdomain')}
hl={'en'}
isValid={true}
value={undefined}
/>
).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(
<ThirdPartyCaptcha
provider={captcha.get('provider')}
sitekey={captcha.get('siteKey')}
clientSubdomain={captcha.get('clientSubdomain')}
hl={'en'}
isValid={true}
value={undefined}
/>
).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)
});
});
});
});
1 change: 1 addition & 0 deletions src/connection/captcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
1 change: 1 addition & 0 deletions src/connection/passwordless/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down
1 change: 1 addition & 0 deletions src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit ea1c9fc

Please sign in to comment.