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
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"auth0-js": "^9.23.3",
"auth0-js": "^9.26.0",
"auth0-password-policies": "^1.0.2",
"blueimp-md5": "^2.19.0",
"classnames": "^2.3.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ exports[`email passwordless renders a captcha 1`] = `
/>
<div
data-__type="captcha_pane"
data-flow="passwordless"
data-i18n={
{
"html": [Function],
"str": [Function],
}
}
data-isPasswordless={true}
data-lock="model"
data-onReload={[Function]}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ exports[`sms passwordless renders a captcha 1`] = `
/>
<div
data-__type="captcha_pane"
data-flow="passwordless"
data-i18n={
{
"html": [Function],
"str": [Function],
}
}
data-isPasswordless={true}
data-lock="model"
data-onReload={[Function]}
/>
Expand Down
71 changes: 52 additions & 19 deletions src/connection/captcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,45 @@ import * as i18n from '../i18n';
import { swap, updateEntity } from '../store/index';
import webApi from '../core/web_api';

export const Flow = Object.freeze({
DEFAULT: 'default',
PASSWORDLESS: 'passwordless',
PASSWORD_RESET: 'password_reset',
});

/**
* Return the captcha config object based on the type of flow.
*
* @param {Object} m model
* @param {Flow} flow Which flow the captcha is being rendered in
*/
export function getCaptchaConfig(m, flow) {
if (flow === Flow.PASSWORD_RESET) {
return l.passwordResetCaptcha(m);
} else if (flow === Flow.PASSWORDLESS) {
return l.passwordlessCaptcha(m);
} else {
return l.captcha(m);
}
}

/**
* 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 {Flow} flow Which flow the captcha is being rendered in
*/
export function showMissingCaptcha(m, id, isPasswordless = false) {
const captchaConfig = isPasswordless ? l.passwordlessCaptcha(m) : l.captcha(m);
export function showMissingCaptcha(m, id, flow = Flow.DEFAULT) {
const captchaConfig = getCaptchaConfig(m, flow);

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 @@ -37,20 +60,20 @@ 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 {Flow} flow Which flow the captcha is being rendered in
* @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, flow, fields) {
const captchaConfig = getCaptchaConfig(m, flow);
const isCaptchaRequired = captchaConfig && captchaConfig.get('required');

if (!isCaptchaRequired) {
return true;
}
const captcha = c.getFieldValue(m, 'captcha');
//captcha required and missing
// captcha required and missing
if (!captcha) {
return false;
}
Expand All @@ -64,12 +87,21 @@ export function setCaptchaParams(m, params, isPasswordless, fields) {
* Get a new challenge and display the new captcha image.
*
* @param {number} id The id of the Lock instance.
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow.
* @param {Flow} flow Which flow the captcha is being rendered in.
* @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, flow, wasInvalid, next) {
if (flow === Flow.PASSWORD_RESET) {
return webApi.getPasswordResetChallenge(id, (err, newCaptcha) => {
if (!err && newCaptcha) {
swap(updateEntity, 'lock', id, l.setPasswordResetCaptcha, newCaptcha, wasInvalid);
}
if (next) {
next();
}
});
} else if (flow === Flow.PASSWORDLESS) {
return webApi.getPasswordlessChallenge(id, (err, newCaptcha) => {
if (!err && newCaptcha) {
swap(updateEntity, 'lock', id, l.setPasswordlessCaptcha, newCaptcha, wasInvalid);
Expand All @@ -78,13 +110,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();
}
});
}
36 changes: 26 additions & 10 deletions src/connection/database/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from './index';

import * as i18n from '../../i18n';
import { setCaptchaParams, showMissingCaptcha, swapCaptcha } from '../captcha';
import { Flow, setCaptchaParams, showMissingCaptcha, swapCaptcha } from '../captcha';

export function logIn(id, needsMFA = false) {
const m = read(getEntity, 'lock', id);
Expand All @@ -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, Flow.DEFAULT, 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, Flow.DEFAULT, 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, Flow.DEFAULT, 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, Flow.DEFAULT, 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, Flow.DEFAULT, 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, Flow.PASSWORD_RESET, ['email']);
if (!isCaptchaValid) {
return showMissingCaptcha(m, id, Flow.PASSWORD_RESET);
}

webApi.resetPassword(id, params, error => {
if (error) {
setTimeout(() => resetPasswordError(id, error), 250);
} else {
Expand Down Expand Up @@ -280,12 +285,23 @@ function resetPasswordSuccess(id) {

function resetPasswordError(id, error) {
const m = read(getEntity, 'lock', id);
let key = error.code;

if (error.code === 'invalid_captcha') {
const captchaConfig = l.passwordResetCaptcha(m);
key = (
captchaConfig.get('provider') === 'recaptcha_v2' ||
captchaConfig.get('provider') === 'recaptcha_enterprise'
) ? 'invalid_recaptcha' : 'invalid_captcha';
}

const errorMessage =
i18n.html(m, ['error', 'forgotPassword', error.code]) ||
i18n.html(m, ['error', 'forgotPassword', key]) ||
i18n.html(m, ['error', 'forgotPassword', 'lock.fallback']);

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

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

export function showLoginActivity(id, fields = ['password']) {
Expand Down
4 changes: 2 additions & 2 deletions src/connection/database/login_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import EmailPane from '../../field/email/email_pane';
import UsernamePane from '../../field/username/username_pane';
import PasswordPane from '../../field/password/password_pane';
import { showResetPasswordActivity } from './actions';
import { swapCaptcha } from '../captcha';
import { Flow, swapCaptcha } from '../captcha';
import { hasScreen, forgotPasswordLink } from './index';
import * as l from '../../core/index';
import CaptchaPane from '../../field/captcha/captcha_pane';
Expand Down 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), Flow.DEFAULT, 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 { Flow, 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.passwordResetCaptcha(lock) &&
l.passwordResetCaptcha(lock).get('required') ? (
<CaptchaPane i18n={i18n} lock={lock} flow={Flow.PASSWORD_RESET} onReload={() => swapCaptcha(l.id(lock), Flow.PASSWORD_RESET, 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
6 changes: 3 additions & 3 deletions src/connection/enterprise/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getFieldValue, hideInvalidFields } from '../../field/index';
import { emailLocalPart } from '../../field/email';
import { logIn as coreLogIn } from '../../core/actions';
import * as l from '../../core/index';
import { setCaptchaParams, showMissingCaptcha, swapCaptcha } from '../captcha';
import { Flow, setCaptchaParams, showMissingCaptcha, swapCaptcha } from '../captcha';

// TODO: enterprise connections should not depend on database
// connections. However, we now allow a username input to contain also
Expand Down 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, Flow.DEFAULT, 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, Flow.DEFAULT, wasCaptchaInvalid, next);
}
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/connection/enterprise/hrd_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import UsernamePane from '../../field/username/username_pane';
import PasswordPane from '../../field/password/password_pane';
import CaptchaPane from '../../field/captcha/captcha_pane';
import { swapCaptcha } from '../captcha';
import { Flow, swapCaptcha } from '../captcha';
import * as l from '../../core/index';

export default class HRDPane extends React.Component {
Expand All @@ -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), Flow.DEFAULT, false)} />
) : null;

return (
Expand Down
Loading