Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IAMRISK-2916] Added support for Auth0 v2 captcha provider #2503

Merged
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
56 changes: 55 additions & 1 deletion src/__tests__/field/captcha.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mount } from 'enzyme';
import I from 'immutable';

import CaptchaPane from '../../field/captcha/captcha_pane';
import { ThirdPartyCaptcha } from '../../field/captcha/third_party_captcha';
import { ThirdPartyCaptcha, getRenderParams } from '../../field/captcha/third_party_captcha';
import CaptchaInput from '../../ui/input/captcha_input';

const createLockMock = ({ provider = 'auth0', required = true, siteKey = '', clientSubdomain = '' } = {}) =>
Expand Down Expand Up @@ -100,6 +100,60 @@ 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');
});

it('renderParams auth0_v2', () => {
const renderParams = getRenderParams({
props: { provider: 'auth0_v2', hl: 'en', sitekey: 'mySiteKey' },
changeHandler: () => {},
expiredHandler: () => {},
erroredHandler: () => {}
});
expect(renderParams).toMatchObject({
sitekey: 'mySiteKey',
language: 'en',
callback: expect.any(Function),
'expired-callback': expect.any(Function),
'error-callback': expect.any(Function),
theme: 'light'
});
});

it('renderParams not auth0_v2', () => {
const renderParams = getRenderParams({
props: { provider: 'not_auth0_v2', hl: 'en', sitekey: 'mySiteKey' },
changeHandler: () => {},
expiredHandler: () => {},
erroredHandler: () => {}
});
expect(renderParams).toMatchObject({
sitekey: 'mySiteKey',
callback: expect.any(Function),
'expired-callback': expect.any(Function),
'error-callback': expect.any(Function)
});
});
});

describe('recaptcha enterprise', () => {
let wrapper;
beforeAll(() => {
Expand Down
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();
});
});
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
32 changes: 25 additions & 7 deletions src/field/captcha/third_party_captcha.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;

Expand All @@ -17,7 +18,8 @@
|| 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) {
Expand All @@ -31,9 +33,25 @@
return window.friendlyChallenge;
case ARKOSE_PROVIDER:
return window.arkose;
case AUTH0_V2_CAPTCHA_PROVIDER:
return window.turnstile;

Check warning on line 37 in src/field/captcha/third_party_captcha.jsx

View check run for this annotation

Codecov / codecov/patch

src/field/captcha/third_party_captcha.jsx#L37

Added line #L37 was not covered by tests
}
};

export const getRenderParams = (self) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this as part of the React component? We shouldn't be passing around this outside the component when we can have it as a class method for the component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code coverage made me do it! I can pass the individual params instead of this but there was no good way (that I could find) to get coverage without pulling it out. Any suggestions?

Copy link
Contributor

@srijonsaha srijonsaha Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm I feel like we shouldn't really do this just to pass code coverage check. My suggestion would be to not create the getRenderParams function just for code coverage and see if we can bypass the code coverage requirement to merge this and see what SDKs has to say because most of the code here is untested anyway. If we do want to test for code coverage sake, I was looking this up and found this. I guess one thing you could do to test it is if there's a mock window object, then you can follow that to do what the stackoverflow answer does and that would call componentDidMount -> injectCaptchaScript -> attaches callback function that calls getRenderParams to window -> call callback from window function to unit test. But I like your current approach better. Only thing I would change is instead of individually passing the render params, can we pass an object that contains the render params so it's extensible for the future if we add new providers that have new params.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if this implementation works for you!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a review comment around this specifically. I do not want to be blocking, but I believe this can be improved.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because most of the code here is untested anyway

I think this shouldnt mean we can not try our best to improve the quality and add tests for new added functionality. Leaving that to you, as you own the functionality, but I would recommend ensuring things are tested.

const renderParams = {
callback: self.changeHandler,
'expired-callback': self.expiredHandler,
'error-callback': self.erroredHandler,
sitekey: self.props.sitekey
}
if (self.props.provider === AUTH0_V2_CAPTCHA_PROVIDER) {
renderParams.language = self.props.hl;
renderParams.theme = 'light';
}
return renderParams;
};

const scriptForProvider = (provider, lang, callback, clientSubdomain, siteKey) => {
switch (provider) {
case RECAPTCHA_V2_PROVIDER:
Expand All @@ -46,6 +64,8 @@
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}`;
}
};

Expand All @@ -61,6 +81,8 @@
return 'friendly-captcha';
case ARKOSE_PROVIDER:
return 'arkose';
case AUTH0_V2_CAPTCHA_PROVIDER:
return 'auth0-v2';
}
};

Expand Down Expand Up @@ -199,13 +221,9 @@
errorCallback: this.erroredHandler,
});
} else {
const renderParams = getRenderParams(this);

Check warning on line 224 in src/field/captcha/third_party_captcha.jsx

View check run for this annotation

Codecov / codecov/patch

src/field/captcha/third_party_captcha.jsx#L224

Added line #L224 was not covered by tests
// 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, renderParams);

Check warning on line 226 in src/field/captcha/third_party_captcha.jsx

View check run for this annotation

Codecov / codecov/patch

src/field/captcha/third_party_captcha.jsx#L226

Added line #L226 was not covered by tests
}
});
}
Expand Down