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

Add support for google recaptcha #1845

Merged
merged 13 commits into from
May 7, 2020
58 changes: 58 additions & 0 deletions src/__tests__/field/captcha.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { mount } from 'enzyme';
import I from 'immutable';

import CaptchaPane from '../../field/captcha/captcha_pane';
import RecaptchaV2 from '../../field/captcha/recaptchav2';
import CaptchaInput from '../../ui/input/captcha_input';

const createLockMock = ({ provider = 'none', siteKey = '' } = {}) =>
I.fromJS({
id: '__lock-id__',
core: {
captcha: { provider, siteKey }
}
});

const createI18nMock = () => ({
str: jest.fn().mockReturnValue('My i18N Compliant Language')
});

describe('CaptchaPane', () => {
describe('CaptchaInput', () => {
let wrapper;
beforeAll(() => {
const lockMock = createLockMock();
const i8nMock = createI18nMock();
const onReloadMock = jest.fn();

wrapper = mount(<CaptchaPane lock={lockMock} onReload={onReloadMock} i18n={i8nMock} />);
});

it('should render CaptchaInput if no provider is specified', () => {
expect(wrapper.find(CaptchaInput)).toHaveLength(1);
});
});

describe('recaptchav2', () => {
let wrapper;
beforeAll(() => {
const lockMock = createLockMock({
provider: 'recaptcha_v2',
siteKey: 'mySiteKey'
});
const i8nMock = createI18nMock();
const onReloadMock = jest.fn();

wrapper = mount(<CaptchaPane lock={lockMock} onReload={onReloadMock} i18n={i8nMock} />);
});

it('should render reCaptcha if provider is recaptchav2', () => {
expect(wrapper.find(RecaptchaV2)).toHaveLength(1);
});

it('should pass the sitekey', () => {
expect(wrapper.find(RecaptchaV2).props().siteKey).toBe('mySiteKey');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`RecaptchaV2 should match the snapshot 1`] = `
ShallowWrapper {
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <Unknown
lock={
Immutable.Map {
"id": "__lock-id__",
"core": Immutable.Map {
"captcha": Immutable.Map {
"provider": "recaptchav2",
"siteKey": "mySiteKey",
},
"transient": Immutable.Map {
"ui": Immutable.Map {
"language": "en-US",
},
},
},
}
}
siteKey="mySiteKey"
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
"nodeType": "host",
"props": Object {
"className": "auth0-lock-recaptchav2",
"style": Object {
"position": "relative",
"transform": "scale(0.86)",
"transformOrigin": "0 0",
},
},
"ref": [Function],
"rendered": null,
"type": "div",
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "host",
"props": Object {
"className": "auth0-lock-recaptchav2",
"style": Object {
"position": "relative",
"transform": "scale(0.86)",
"transformOrigin": "0 0",
},
},
"ref": [Function],
"rendered": null,
"type": "div",
},
],
Symbol(enzyme.__options__): Object {
"adapter": ReactFifteenAdapter {
"options": Object {
"legacyContextMode": "parent",
"lifecycles": Object {
"componentDidUpdate": Object {
"prevContext": true,
},
"getChildContext": Object {
"calledByRenderer": true,
},
},
"supportPrevContextArgumentOfComponentDidUpdate": true,
},
},
Symbol(enzyme.__providerValues__): undefined,
},
Symbol(enzyme.__providerValues__): Map {},
Symbol(enzyme.__childContext__): null,
}
`;
42 changes: 42 additions & 0 deletions src/__tests__/field/captcha/recaptchav2.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { shallow } from 'enzyme';
import I from 'immutable';

import RecaptchaV2, { render } from '../../../field/captcha/recaptchav2';

const createLockMock = ({ provider = 'none', siteKey = '' } = {}) =>
I.fromJS({
id: '__lock-id__',
core: {
captcha: { provider, siteKey },
transient: {
ui: {
language: 'en-US'
}
}
}
});

describe('RecaptchaV2', () => {
it('should match the snapshot', () => {
const mockLock = createLockMock({ provider: 'recaptchav2', siteKey: 'mySiteKey' });
const wrapper = shallow(<RecaptchaV2 lock={mockLock} siteKey={'mySiteKey'} />);
expect(wrapper).toMatchSnapshot();
});

describe('render', () => {
beforeAll(() => {
document.body.innerHTML = "<div id='renderTest'></div>";
});
afterAll(() => {
document.getElementById('renderTest').remove();
});
it('injects the script', () => {
const mockLock = createLockMock({ provider: 'recaptchav2', siteKey: 'mySiteKey' });
render(mockLock, document.getElementById('renderTest'), {});
expect(document.body.innerHTML).toBe(
'<div id="renderTest"></div><script src="https://www.google.com/recaptcha/api.js?hl=en-US"></script>'
);
});
});
});
76 changes: 42 additions & 34 deletions src/field/captcha/captcha_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,54 @@ import * as l from '../../core/index';
import { swap, updateEntity } from '../../store/index';
import * as captchaField from '../captcha';
import { getFieldValue, isFieldVisiblyInvalid } from '../index';

const Captcha = ({ lock, i18n, onReload }) => {
const lockId = l.id(lock);

function handleChange(e) {
swap(updateEntity, 'lock', lockId, captchaField.set, e.target.value);
import RecaptchaV2 from './recaptchav2';

export default class CaptchaPane extends React.Component {
render() {
const { i18n, lock, onReload } = this.props;

const lockId = l.id(lock);

function handleChange(e) {
swap(updateEntity, 'lock', lockId, captchaField.set, e.target.value);
}

const captcha = l.captcha(lock);

if (captcha.get('provider') === 'recaptcha_v2') {
return <RecaptchaV2 lock={lock} siteKey={captcha.get('siteKey')} />;
}

const placeholder =
captcha.get('type') === 'code'
? i18n.str(`captchaCodeInputPlaceholder`)
: i18n.str(`captchaMathInputPlaceholder`);

const value = getFieldValue(lock, 'captcha');
const isValid = !isFieldVisiblyInvalid(lock, 'captcha');

return (
<CaptchaInput
lockId={lockId}
image={captcha.get('image')}
placeholder={placeholder}
isValid={isValid}
onChange={handleChange}
onReload={onReload}
value={value}
invalidHint={i18n.str('blankErrorHint')}
/>
);
}
}

const captcha = l.captcha(lock);

const placeholder =
captcha.get('type') === 'code'
? i18n.str(`captchaCodeInputPlaceholder`)
: i18n.str(`captchaMathInputPlaceholder`);

const value = getFieldValue(lock, 'captcha');
const isValid = !isFieldVisiblyInvalid(lock, 'captcha');

return (
<CaptchaInput
lockId={lockId}
image={captcha.get('image')}
placeholder={placeholder}
isValid={isValid}
onChange={handleChange}
onReload={onReload}
value={value}
invalidHint={i18n.str('blankErrorHint')}
/>
);
};

Captcha.propTypes = {
CaptchaPane.propTypes = {
i18n: PropTypes.object.isRequired,
lock: PropTypes.object.isRequired,
error: PropTypes.bool,
onReload: PropTypes.func.isRequired
};

Captcha.defaultProps = {
CaptchaPane.defaultProps = {
error: false
};

export default Captcha;
67 changes: 67 additions & 0 deletions src/field/captcha/recaptchav2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { Set } from 'immutable';
import * as l from '../../core/index';
import { swap, updateEntity } from '../../store';
import * as captcha from '../captcha';

function isScriptAvailable(scriptUrl) {
//check the window object
if (window.grecaptcha && typeof window.grecaptcha.render === 'function') {
return true;
}
//check the scripts element, it might be loading
const allScripts = new Set(document.scripts);
return allScripts.some(s => s.src === scriptUrl);
}

function injectGoogleCaptchaIfMissing(lock) {
const lang = l.ui.language(lock);
const scriptUrl = `https://www.google.com/recaptcha/api.js?hl=${lang}`;
if (isScriptAvailable(scriptUrl)) {
return;
}
const script = document.createElement('script');
script.src = scriptUrl;
script.async = true;
document.body.appendChild(script);
}

/**
* waits until google recaptcha is ready and renders
*/
function renderElement(lock, el, prop) {
if (!window.grecaptcha || typeof window.grecaptcha.render !== 'function') {
return setTimeout(() => renderElement(lock, el, prop), 100);
}

const id = l.id(lock);
try {
window.grecaptcha.render(el, {
callback: value => {
swap(updateEntity, 'lock', id, captcha.set, value, false);
},
'expired-callback': () => {
swap(updateEntity, 'lock', id, captcha.reset);
},
...prop
});
} catch (err) {}
}

export function render(lock, element, properties) {
if (!element || element.innerHTML !== '') {
return;
}

injectGoogleCaptchaIfMissing(lock);

renderElement(lock, element, properties);
}

export default ({ lock, siteKey }) => (
<div
style={{ transform: 'scale(0.86)', transformOrigin: '0 0', position: 'relative' }}
className="auth0-lock-recaptchav2"
ref={el => render(lock, el, { sitekey: siteKey })}
/>
);
2 changes: 1 addition & 1 deletion src/ui/box/chrome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export default class Chrome extends React.Component {
The submit button should always be included in the DOM.
Otherwise, password managers will call `form.submit()`,
which doesn't trigger the `onsubmit` event handler, which
makes impossible for react to handle the submit event,
makes impossible for react to handle the submit event,
causing the page to send a POST request to `window.location.href`
with all the form data.
*/}
Expand Down
Loading