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-2603] Add support for Arkose #2455

Merged
merged 5 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/field/captcha/captcha_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class CaptchaPane extends React.Component {
<ThirdPartyCaptcha
provider={provider}
sitekey={captcha.get('siteKey')}
clientSubdomain={captcha.get('clientSubdomain')}
onChange={handleChange}
onExpired={reset}
hl={l.ui.language(lock)}
Expand Down
136 changes: 111 additions & 25 deletions src/field/captcha/third_party_captcha.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
const RECAPTCHA_ENTERPRISE_PROVIDER = 'recaptcha_enterprise';
const HCAPTCHA_PROVIDER = 'hcaptcha';
const FRIENDLY_CAPTCHA_PROVIDER = 'friendly_captcha';
const ARKOSE_PROVIDER = 'arkose';
const TIMEOUT_MS = 500;
const MAX_RETRY = 3;

export const isThirdPartyCaptcha = provider =>
provider === RECAPTCHA_ENTERPRISE_PROVIDER
|| provider === RECAPTCHA_V2_PROVIDER
|| provider === HCAPTCHA_PROVIDER
|| provider === FRIENDLY_CAPTCHA_PROVIDER;
|| provider === FRIENDLY_CAPTCHA_PROVIDER
|| provider === ARKOSE_PROVIDER;

const getCaptchaProvider = provider => {
switch (provider) {
Expand All @@ -25,10 +29,12 @@
return window.hcaptcha;
case FRIENDLY_CAPTCHA_PROVIDER:
return window.friendlyChallenge;
case ARKOSE_PROVIDER:
return window.arkose;
}
};

const scriptForProvider = (provider, lang, callback) => {
const scriptForProvider = (provider, lang, callback, clientSubdomain, siteKey) => {
switch (provider) {
case RECAPTCHA_V2_PROVIDER:
return `https://www.recaptcha.net/recaptcha/api.js?hl=${lang}&onload=${callback}`;
Expand All @@ -38,6 +44,8 @@
return `https://js.hcaptcha.com/1/api.js?hl=${lang}&onload=${callback}`;
case FRIENDLY_CAPTCHA_PROVIDER:
return 'https://cdn.jsdelivr.net/npm/[email protected]/widget.min.js';
case ARKOSE_PROVIDER:
return 'https://' + clientSubdomain + '.arkoselabs.com/v2/' + siteKey + '/api.js';
}
};

Expand All @@ -51,13 +59,40 @@
return 'hcaptcha';
case FRIENDLY_CAPTCHA_PROVIDER:
return 'friendly-captcha';
case ARKOSE_PROVIDER:
return 'arkose';
}
}
};

const loadScript = (url, attributes) => {
var script = window.document.createElement('script');
for (var attr in attributes) {
if (attr.startsWith('data-')) {
script.dataset[attr.replace('data-', '')] = attributes[attr];
} else {
script[attr] = attributes[attr];
}
}
script.src = url;
document.body.appendChild(script);
};

const removeScript = (url) => {
var scripts = window.document.querySelectorAll('script[src="' + url + '"]');
scripts.forEach(function (script) {
script.remove();
});
};

export class ThirdPartyCaptcha extends React.Component {

Check warning

Code scanning / CodeQL

Unused or undefined state property Warning

Component state property 'retryCount' is
written
, but it is never read.

constructor(props) {
super(props);
this.state = {};
console.log(props);
srijonsaha marked this conversation as resolved.
Show resolved Hide resolved
this.state = {
retryCount: 0,
scriptUrl: ''
};
//this version of react doesn't have React.createRef
this.ref = createRef();

Expand All @@ -84,39 +119,88 @@
};
}

static loadScript(props, element = document.body, callback = noop) {
const callbackName = `${providerDomPrefix(props.provider)}Callback_${Math.floor(Math.random() * 1000001)}`;
const scriptUrl = scriptForProvider(props.provider, props.hl, callbackName);
const script = document.createElement('script');

window[callbackName] = () => {
delete window[callbackName];
callback(null, script);
injectCaptchaScript(callback = noop) {
const { provider, hl, clientSubdomain, sitekey } = this.props;
const callbackName = `${providerDomPrefix(provider)}Callback_${Math.floor(Math.random() * 1000001)}`;
const scriptUrl = scriptForProvider(provider, hl, callbackName, clientSubdomain, sitekey);
const attributes = {
async: true,
defer: true
};

script.src = scriptUrl;
script.async = true;
script.defer = true;
if (props.provider === FRIENDLY_CAPTCHA_PROVIDER) {
script.onload = window[callbackName];
if (provider === ARKOSE_PROVIDER) {
attributes['data-callback'] = callbackName;
attributes['onerror'] = function () {
if (this.state.retryCount < MAX_RETRY) {
removeScript(scriptUrl);
loadScript(scriptUrl, attributes);
this.setState((prevState) => ({
retryCount: prevState.retryCount + 1
}));
return;
}
removeScript(scriptUrl);
this.changeHandler('BYPASS_CAPTCHA');
};
window[callbackName] = function (arkose) {
callback(arkose);
};
} else {
window[callbackName] = () => {
delete window[callbackName];
callback();
};

if (provider === FRIENDLY_CAPTCHA_PROVIDER) {
attributes['onload'] = window[callbackName];
}
}

element.appendChild(script);
loadScript(scriptUrl, attributes);
}

componentWillUnmount() {
if (!this.scriptNode) {
if (!this.state.scriptUrl) {
srijonsaha marked this conversation as resolved.
Show resolved Hide resolved
return;
}
document.body.removeChild(this.scriptNode);
removeScript(this.state.scriptUrl);
}

componentDidMount() {
ThirdPartyCaptcha.loadScript(this.props, document.body, (err, scriptNode) => {
this.scriptNode = scriptNode;
this.injectCaptchaScript((arkose) => {
const provider = getCaptchaProvider(this.props.provider);

if (this.props.provider === FRIENDLY_CAPTCHA_PROVIDER) {
if (this.props.provider === ARKOSE_PROVIDER) {
var handleArkose = function (event) {
setTimeout(function () {
arkose.run();
}, TIMEOUT_MS);
srijonsaha marked this conversation as resolved.
Show resolved Hide resolved
// TODO: This should prevent the form from submitting
srijonsaha marked this conversation as resolved.
Show resolved Hide resolved
event.preventDefault();
};
var getFormElement = function () {
return window.document.querySelector('form.auth0-lock-widget');
};
getFormElement().addEventListener('submit', handleArkose);
arkose.setConfig({
onCompleted: function (response) {
this.changeHandler(response.token);
getFormElement().submit();
},
onError: function () {
if (this.state.retryCount < MAX_RETRY) {
arkose.reset();
// To ensure reset is successful, we need to set a timeout here
setTimeout(function () {
arkose.run();
}, TIMEOUT_MS);
this.setState((prevState) => ({
retryCount: prevState.retryCount + 1
}));
}
this.changeHandler('BYPASS_CAPTCHA');
getFormElement().removeEventListener('submit', handleArkose);
}
});
} else if (this.props.provider === FRIENDLY_CAPTCHA_PROVIDER) {
this.widgetInstance = new provider.WidgetInstance(this.ref.current, {
sitekey: this.props.sitekey,
language: this.props.hl,
Expand Down Expand Up @@ -145,6 +229,7 @@
provider.reset(this.widgetId);
}
}

render() {
/*
This is an override for the following conflicting css-rule:
Expand Down Expand Up @@ -212,6 +297,7 @@
ThirdPartyCaptcha.propTypes = {
provider: propTypes.string.isRequired,
sitekey: propTypes.string.isRequired,
clientSubdomain: propTypes.string,
onChange: propTypes.func,
onExpired: propTypes.func,
onErrored: propTypes.func,
Expand Down
Loading