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

Support captchas in reset password flow #2547

Merged
merged 11 commits into from
Apr 30, 2024
64 changes: 48 additions & 16 deletions src/connection/captcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,41 @@ import * as i18n from '../i18n';
import { swap, updateEntity } from '../store/index';
import webApi from '../core/web_api';

/**
* Return the captcha config object based on the type of flow.
*
* @param {Object} m model
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow
* @param {Boolean} isPasswordReset Whether the captcha is being rendered in a password reset flow
*/
export function getCaptchaConfig(m, isPasswordless, isPasswordReset) {
if (isPasswordReset) {
return l.resetPasswordCaptcha(m);
} else if (isPasswordless) {
return l.passwordlessCaptcha(m);
} else {
return l.captcha(m);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this is a great API for this function. The two input booleans are actually mutually exclusive when looking at the implementation but someone calling this method would reasonably expect that they can provide any combination of true and false here and each combination would have its own result.

I can see this pattern used throughout the PR - why was this decision taken vs using something else, maybe a number value and some constants?

Copy link
Contributor Author

@srijonsaha srijonsaha Apr 25, 2024

Choose a reason for hiding this comment

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

I just tried to follow the existing pattern with the isPasswordless boolean but you're right it could be confusing to the caller. I'll try to change it to a single value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated


/**
* Display the error message of missing captcha in the header of lock.
*
* @param {Object} m model
* @param {Number} id
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow
* @param {Boolean} isPasswordReset Whether the captcha is being rendered in a password reset flow
*/
export function showMissingCaptcha(m, id, isPasswordless = false) {
const captchaConfig = isPasswordless ? l.passwordlessCaptcha(m) : l.captcha(m);
export function showMissingCaptcha(m, id, isPasswordless = false, isPasswordReset = false) {
const captchaConfig = getCaptchaConfig(m, isPasswordless, isPasswordReset);

const captchaError = (
captchaConfig.get('provider') === 'recaptcha_v2' ||
captchaConfig.get('provider') === 'recaptcha_enterprise' ||
captchaConfig.get('provider') === 'hcaptcha' ||
captchaConfig.get('provider') === 'auth0_v2' ||
captchaConfig.get('provider') === 'friendly_captcha'
captchaConfig.get('provider') === 'friendly_captcha' ||
captchaConfig.get('provider') === 'arkose'
) ? 'invalid_recaptcha' : 'invalid_captcha';

const errorMessage = i18n.html(m, ['error', 'login', captchaError]);
Expand All @@ -38,19 +57,21 @@ export function showMissingCaptcha(m, id, isPasswordless = false) {
* @param {Object} m model
* @param {Object} params
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow
* @param {Boolean} isPasswordReset Whether the captcha is being rendered in a password reset flow
* @param {Object} fields
*
* @returns {Boolean} returns true if is required and missing the response from the user
*/
export function setCaptchaParams(m, params, isPasswordless, fields) {
const captchaConfig = isPasswordless ? l.passwordlessCaptcha(m) : l.captcha(m);
export function setCaptchaParams(m, params, isPasswordless, isPasswordReset, fields) {
const captchaConfig = getCaptchaConfig(m, isPasswordless, isPasswordReset);
const isCaptchaRequired = captchaConfig && captchaConfig.get('required');

if (!isCaptchaRequired) {
return true;
}
const captcha = c.getFieldValue(m, 'captcha');
//captcha required and missing
console.log('captcha: ', captcha);
// captcha required and missing
if (!captcha) {
return false;
}
Expand All @@ -65,11 +86,21 @@ export function setCaptchaParams(m, params, isPasswordless, fields) {
*
* @param {number} id The id of the Lock instance.
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow.
* @param {Boolean} isPasswordReset Whether the captcha is being rendered in a password reset flow.
* @param {boolean} wasInvalid A boolean indicating if the previous captcha was invalid.
* @param {Function} [next] A callback.
*/
export function swapCaptcha(id, isPasswordless, wasInvalid, next) {
if (isPasswordless) {
export function swapCaptcha(id, isPasswordless, isPasswordReset, wasInvalid, next) {
if (isPasswordReset) {
return webApi.getResetPasswordChallenge(id, (err, newCaptcha) => {
if (!err && newCaptcha) {
swap(updateEntity, 'lock', id, l.setResetPasswordCaptcha, newCaptcha, wasInvalid);
}
if (next) {
next();
}
});
} else if (isPasswordless) {
return webApi.getPasswordlessChallenge(id, (err, newCaptcha) => {
if (!err && newCaptcha) {
swap(updateEntity, 'lock', id, l.setPasswordlessCaptcha, newCaptcha, wasInvalid);
Expand All @@ -78,13 +109,14 @@ export function swapCaptcha(id, isPasswordless, wasInvalid, next) {
next();
}
});
} else {
return webApi.getChallenge(id, (err, newCaptcha) => {
if (!err && newCaptcha) {
swap(updateEntity, 'lock', id, l.setCaptcha, newCaptcha, wasInvalid);
}
if (next) {
next();
}
});
}
return webApi.getChallenge(id, (err, newCaptcha) => {
if (!err && newCaptcha) {
swap(updateEntity, 'lock', id, l.setCaptcha, newCaptcha, wasInvalid);
}
if (next) {
next();
}
});
}
23 changes: 15 additions & 8 deletions src/connection/database/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function logIn(id, needsMFA = false) {
};

const fields = [usernameField, 'password'];
const isCaptchaValid = setCaptchaParams(m, params, false, fields);
const isCaptchaValid = setCaptchaParams(m, params, false, false, fields);

if (!isCaptchaValid) {
return showMissingCaptcha(m, id);
Expand All @@ -53,7 +53,7 @@ export function logIn(id, needsMFA = false) {

if (error) {
const wasInvalid = error && error.code === 'invalid_captcha';
return swapCaptcha(id, false, wasInvalid, next);
return swapCaptcha(id, false, false, wasInvalid, next);
}

next();
Expand Down Expand Up @@ -88,7 +88,7 @@ export function signUp(id) {
autoLogin: shouldAutoLogin(m)
};

const isCaptchaValid = setCaptchaParams(m, params, false, fields);
const isCaptchaValid = setCaptchaParams(m, params, false, false, fields);
if (!isCaptchaValid) {
return showMissingCaptcha(m, id);
}
Expand Down Expand Up @@ -131,7 +131,7 @@ export function signUp(id) {

const wasInvalidCaptcha = error && error.code === 'invalid_captcha';

swapCaptcha(id, false, wasInvalidCaptcha, () => {
swapCaptcha(id, false, false, wasInvalidCaptcha, () => {
setTimeout(() => signUpError(id, error), 250);
});
};
Expand Down Expand Up @@ -218,7 +218,7 @@ export function signUpError(id, error) {

if (errorKey === 'invalid_captcha') {
errorMessage = i18n.html(m, ['error', 'login', errorKey]);
return swapCaptcha(id, false, true, () => {
return swapCaptcha(id, false, false, true, () => {
swap(updateEntity, 'lock', id, l.setSubmitting, false, errorMessage);
});
}
Expand All @@ -244,7 +244,12 @@ export function resetPassword(id) {
email: c.getFieldValue(m, 'email')
};

webApi.resetPassword(id, params, (error, ...args) => {
const isCaptchaValid = setCaptchaParams(m, params, false, true, ['email']);
if (!isCaptchaValid) {
return showMissingCaptcha(m, id, false, true);
}

webApi.resetPassword(id, params, error => {
if (error) {
setTimeout(() => resetPasswordError(id, error), 250);
} else {
Expand Down Expand Up @@ -284,8 +289,10 @@ function resetPasswordError(id, error) {
const errorMessage =
i18n.html(m, ['error', 'forgotPassword', error.code]) ||
i18n.html(m, ['error', 'forgotPassword', 'lock.fallback']);

swap(updateEntity, 'lock', id, l.setSubmitting, false, errorMessage);

swapCaptcha(id, false, true, error.code === 'invalid_captcha', () => {
swap(updateEntity, 'lock', id, l.setSubmitting, false, errorMessage);
});
}

export function showLoginActivity(id, fields = ['password']) {
Expand Down
2 changes: 1 addition & 1 deletion src/connection/database/login_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default class LoginPane extends React.Component {
l.captcha(lock) &&
l.captcha(lock).get('required') &&
(isHRDDomain(lock, databaseUsernameValue(lock)) || !sso) ? (
<CaptchaPane i18n={i18n} lock={lock} onReload={() => swapCaptcha(l.id(lock), false, false)} />
<CaptchaPane i18n={i18n} lock={lock} onReload={() => swapCaptcha(l.id(lock), false, false, false)} />
) : null;

const dontRememberPassword =
Expand Down
9 changes: 9 additions & 0 deletions src/connection/database/reset_password_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import EmailPane from '../../field/email/email_pane';
import * as l from '../../core/index';
import CaptchaPane from '../../field/captcha/captcha_pane';
import { swapCaptcha } from '../../connection/captcha';

export default class ResetPasswordPane extends React.Component {
static propTypes = {
Expand All @@ -12,6 +14,12 @@ export default class ResetPasswordPane extends React.Component {
render() {
const { emailInputPlaceholder, header, i18n, lock } = this.props;

const captchaPane =
l.resetPasswordCaptcha(lock) &&
l.resetPasswordCaptcha(lock).get('required') ? (
<CaptchaPane i18n={i18n} lock={lock} onReload={() => swapCaptcha(l.id(lock), false, true, false, null)} />
) : null;

return (
<div>
{header}
Expand All @@ -21,6 +29,7 @@ export default class ResetPasswordPane extends React.Component {
placeholder={emailInputPlaceholder}
strictValidation={false}
/>
{captchaPane}
</div>
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/connection/enterprise/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function logIn(id) {
return logInSSO(id, ssoConnection, params);
}

const isCaptchaValid = setCaptchaParams(m, params, false, fields);
const isCaptchaValid = setCaptchaParams(m, params, false, false, fields);

if (!isCaptchaValid && !ssoConnection) {
return showMissingCaptcha(m, id);
Expand Down Expand Up @@ -85,7 +85,7 @@ function logInActiveFlow(id, params) {
},
(id, error, fields, next) => {
const wasCaptchaInvalid = error && error.code === 'invalid captcha';
swapCaptcha(id, false, wasCaptchaInvalid, next);
swapCaptcha(id, false, false, wasCaptchaInvalid, next);
}
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/connection/enterprise/hrd_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class HRDPane extends React.Component {

const captchaPane =
l.captcha(model) && l.captcha(model).get('required') ? (
<CaptchaPane i18n={i18n} lock={model} onReload={() => swapCaptcha(l.id(model), false, false)} />
<CaptchaPane i18n={i18n} lock={model} onReload={() => swapCaptcha(l.id(model), false, false, false)} />
) : null;

return (
Expand Down
11 changes: 6 additions & 5 deletions src/connection/passwordless/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ function getErrorMessage(m, id, error) {
captchaConfig.get('provider') === 'recaptcha_enterprise' ||
captchaConfig.get('provider') === 'hcaptcha' ||
captchaConfig.get('provider') === 'auth0_v2' ||
captchaConfig.get('provider') === 'friendly_captcha'
captchaConfig.get('provider') === 'friendly_captcha' ||
captchaConfig.get('provider') === 'arkose'
) ? 'invalid_recaptcha' : 'invalid_captcha';
}

Expand All @@ -47,7 +48,7 @@ function getErrorMessage(m, id, error) {

function swapCaptchaAfterError(id, error){
const wasCaptchaInvalid = error && error.code === 'invalid_captcha';
swapCaptcha(id, true, wasCaptchaInvalid);
swapCaptcha(id, true, false, wasCaptchaInvalid);
}

export function requestPasswordlessEmail(id) {
Expand Down Expand Up @@ -102,7 +103,7 @@ function sendEmail(m, id, successFn, errorFn) {
if (isSendLink(m) && !l.auth.params(m).isEmpty()) {
params.authParams = l.auth.params(m).toJS();
}
const isCaptchaValid = setCaptchaParams(m, params, true, []);
const isCaptchaValid = setCaptchaParams(m, params, true, false, []);

if (!isCaptchaValid) {
return showMissingCaptcha(m, id, true);
Expand All @@ -124,7 +125,7 @@ export function sendSMS(id) {
phoneNumber: phoneNumberWithDiallingCode(m),
send: send(m)
};
const isCaptchaValid = setCaptchaParams(m, params, true, []);
const isCaptchaValid = setCaptchaParams(m, params, true, false, []);
if (!isCaptchaValid) {
return showMissingCaptcha(m, id, true);
}
Expand Down Expand Up @@ -187,7 +188,7 @@ export function logIn(id) {

export function restart(id) {
swap(updateEntity, 'lock', id, restartPasswordless);
swapCaptcha(id, true, false);
swapCaptcha(id, true, false, false);
}

export function toggleTermsAcceptance(id) {
Expand Down
12 changes: 11 additions & 1 deletion src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,11 @@ export function setPasswordlessCaptcha(m, value, wasInvalid) {
return set(m, 'passwordlessCaptcha', Immutable.fromJS(value));
}

export function setResetPasswordCaptcha(m, value, wasInvalid) {
m = captchaField.reset(m, wasInvalid);
return set(m, 'resetPasswordCaptcha', Immutable.fromJS(value));
}

export function captcha(m) {
return get(m, 'captcha');
}
Expand All @@ -434,6 +439,10 @@ export function passwordlessCaptcha(m) {
return get(m, 'passwordlessCaptcha');
}

export function resetPasswordCaptcha(m) {
return get(m, 'resetPasswordCaptcha');
}

export function prefill(m) {
return get(m, 'prefill', {});
}
Expand Down Expand Up @@ -585,7 +594,8 @@ export function loginErrorMessage(m, error, type) {
currentCaptcha.get('provider') === 'recaptcha_enterprise' ||
currentCaptcha.get('provider') === 'hcaptcha' ||
currentCaptcha.get('provider') === 'auth0_v2' ||
captchaConfig.get('provider') === 'friendly_captcha'
currentCaptcha.get('provider') === 'friendly_captcha' ||
currentCaptcha.get('provider') === 'arkose'
)) {
code = 'invalid_recaptcha';
}
Expand Down
11 changes: 10 additions & 1 deletion src/core/remote_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as l from './index';
import { isADEnabled } from '../connection/enterprise'; // shouldn't depend on this
import sync, { isSuccess } from '../sync';
import webApi from './web_api';
import { setCaptcha, setPasswordlessCaptcha } from '../core/index';
import { setCaptcha, setPasswordlessCaptcha, setResetPasswordCaptcha } from '../core/index';

export function syncRemoteData(m) {
if (l.useTenantInfo(m)) {
Expand Down Expand Up @@ -69,6 +69,15 @@ export function syncRemoteData(m) {
successFn: setPasswordlessCaptcha
});

m = sync(m, 'resetPasswordCaptcha', {
syncFn: (m, cb) => {
webApi.getResetPasswordChallenge(m.get('id'), (err, r) => {
cb(null, r);
});
},
successFn: setResetPasswordCaptcha
});


return m;
}
4 changes: 4 additions & 0 deletions src/core/web_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class Auth0WebAPI {
return this.clients[lockID].getPasswordlessChallenge(callback);
}

getResetPasswordChallenge(lockID, callback) {
return this.clients[lockID].getResetPasswordChallenge(callback);
}

getSSOData(lockID, ...args) {
return this.clients[lockID].getSSOData(...args);
}
Expand Down
4 changes: 4 additions & 0 deletions src/core/web_api/p2_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ class Auth0APIClient {
return this.client.client.passwordless.getChallenge(...params);
}

getResetPasswordChallenge(...params) {
return this.client.client.dbConnection.getChallenge(...params);
}

getUserCountry(cb) {
return this.client.client.getUserCountry(cb);
}
Expand Down
2 changes: 1 addition & 1 deletion src/engine/classic/sign_up_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default class SignUpPane extends React.Component {
l.captcha(model) &&
l.captcha(model).get('required') &&
(isHRDDomain(model, databaseUsernameValue(model)) || !sso) ? (
<CaptchaPane i18n={i18n} lock={model} onReload={() => swapCaptcha(l.id(model), false, false)} />
<CaptchaPane i18n={i18n} lock={model} onReload={() => swapCaptcha(l.id(model), false, false, false)} />
) : null;

const passwordPane = !onlyEmail && (
Expand Down
2 changes: 1 addition & 1 deletion src/engine/passwordless/social_or_email_login_screen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const Component = ({ i18n, model }) => {

const captchaPane = l.passwordlessCaptcha(model) && l.passwordlessCaptcha(model).get('required')
? (
<CaptchaPane i18n={i18n} lock={model} isPasswordless={true} onReload={() => swapCaptcha(l.id(model), true, false)} />
<CaptchaPane i18n={i18n} lock={model} isPasswordless={true} onReload={() => swapCaptcha(l.id(model), true, false, false)} />
) : null;

return (
Expand Down
Loading
Loading