Skip to content

Commit

Permalink
Add MFA code input screen for /ro endpoint (#628)
Browse files Browse the repository at this point in the history
We will show MFA screen when mfa is required for an user
and the customer is using the /ro endpoint instead of
popup or redirect mode (push notifications not allowed).
  • Loading branch information
dafortune authored and hzalaz committed Oct 17, 2016
1 parent 0ac5377 commit f82d025
Show file tree
Hide file tree
Showing 15 changed files with 516 additions and 19 deletions.
31 changes: 28 additions & 3 deletions src/connection/database/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,33 @@ import {
} from './index';
import * as i18n from '../../i18n';

export function logIn(id) {
export function logIn(id, needsMFA = false) {
const m = read(getEntity, "lock", id);
const usernameField = databaseLogInWithEmail(m) ? "email" : "username";
const username = c.getFieldValue(m, usernameField);

coreLogIn(id, [usernameField, "password"], {
const params = {
connection: databaseConnectionName(m),
username: username,
password: c.getFieldValue(m, "password")
});
};

const fields = [usernameField, "password"];

const mfaCode = c.getFieldValue(m, "mfa_code");
if (needsMFA) {
params["mfa_code"] = mfaCode;
fields.push("mfa_code");
}

coreLogIn(id, fields, params,
(id, error, fields, next) => {
if (error.error === "a0.mfa_required") {
return showLoginMFAActivity(id);
}

return next();
});
}

export function signUp(id) {
Expand Down Expand Up @@ -204,6 +221,14 @@ export function cancelResetPassword(id) {
return showLoginActivity(id);
}

export function cancelMFALogin(id) {
return showLoginActivity(id);
}

export function toggleTermsAcceptance(id) {
swap(updateEntity, "lock", id, switchTermsAcceptance);
}

export function showLoginMFAActivity(id, fields = ["mfa_code"]) {
swap(updateEntity, "lock", id, setScreen, "mfaLogin", fields);
}
4 changes: 3 additions & 1 deletion src/connection/database/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ function processScreenOptions(opts, defaults = {allowLogin: true, allowSignUp: t
screens.push("forgotPassword");
}

screens.push("mfaLogin");

if (!assertMaybeEnum(opts, "initialScreen", screens)) {
initialScreen = undefined;
}
Expand Down Expand Up @@ -251,7 +253,7 @@ export function setScreen(m, name, fields = []) {
export function getScreen(m) {
const screen = tget(m, "screen");
const initialScreen = getInitialScreen(m);
const screens = [screen, initialScreen, "login", "signUp", "forgotPassword"];
const screens = [screen, initialScreen, "login", "signUp", "forgotPassword", "mfaLogin"];
const availableScreens = screens.filter(x => hasScreen(m, x));
return availableScreens[0];
}
Expand Down
37 changes: 37 additions & 0 deletions src/connection/database/mfa_pane.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import MFACodePane from '../../field/mfa-code/mfa_code_pane';

export default class MFAPane extends React.Component {

render() {
const {
mfaInputPlaceholder,
i18n,
instructions,
lock,
title
} = this.props;

const headerText = instructions || null;
const header = headerText && <p>{headerText}</p>;

const pane = (<MFACodePane
i18n={i18n}
lock={lock}
placeholder={mfaInputPlaceholder}
/>);

const titleElement = title && <h2>{ title }</h2>;

return (<div>{titleElement}{header}{pane}</div>);
}

}

MFAPane.propTypes = {
mfaInputPlaceholder: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
i18n: React.PropTypes.object.isRequired,
instructions: React.PropTypes.any,
lock: React.PropTypes.object.isRequired
};
25 changes: 15 additions & 10 deletions src/core/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,20 @@ export function validateAndSubmit(id, fields = [], f) {
}
}

export function logIn(id, fields, params = {}) {
export function logIn(id, fields, params = {},
logInErrorHandler = (err, next) => next()) {

validateAndSubmit(id, fields, m => {
webApi.logIn(id, params, l.auth.params(m).toJS(), (error, result) => {
if (error) {
setTimeout(() => logInError(id, fields, error), 250);
setTimeout(() => logInError(id, fields, error, logInErrorHandler), 250)
} else {
logInSuccess(id, result);
}
});
});
}


export function logInSuccess(id, result) {
const m = read(getEntity, "lock", id);

Expand All @@ -177,15 +178,19 @@ export function logInSuccess(id, result) {
}
}

function logInError(id, fields, error) {
const m = read(getEntity, "lock", id);
const errorMessage = l.loginErrorMessage(m, error, loginType(fields));
function logInError(id, fields, error, localHandler) {
localHandler(id, error, fields, () => process.nextTick(() => {
const m = read(getEntity, "lock", id);
const errorMessage = l.loginErrorMessage(m, error, loginType(fields));

if (["blocked_user", "rule_error", "lock.unauthorized"].indexOf(error.code) > -1) {
l.emitAuthorizationErrorEvent(m, error);
}
if (["blocked_user", "rule_error", "lock.unauthorized"].indexOf(error.code) > -1) {
l.emitAuthorizationErrorEvent(m, error);
}

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

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

function loginType(fields) {
Expand Down
8 changes: 8 additions & 0 deletions src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,14 @@ export function loginErrorMessage(m, error, type) {
code = INVALID_MAP[type];
}

if (code === "a0.mfa_registration_required") {
code = "lock.mfa_registration_required";
}

if (code === "a0.mfa_invalid_code") {
code = "lock.mfa_invalid_code";
}

return i18n.str(m, ["error", "login", code])
|| i18n.str(m, ["error", "login", "lock.fallback"]);
}
Expand Down
4 changes: 3 additions & 1 deletion src/engine/classic.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Base from '../index';
import Login from './classic/login';
import SignUp from './classic/sign_up_screen';
import MFALoginScreen from './classic/mfa_login_screen';
import ResetPassword from '../connection/database/reset_password';
import { renderSSOScreens } from '../core/sso/index';
import {
Expand Down Expand Up @@ -92,7 +93,8 @@ class Classic {
static SCREENS = {
login: Login,
forgotPassword: ResetPassword,
signUp: SignUp
signUp: SignUp,
mfaLogin: MFALoginScreen
};

didInitialize(model, options) {
Expand Down
45 changes: 45 additions & 0 deletions src/engine/classic/mfa_login_screen.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import Screen from '../../core/screen';
import MFAPane from '../../connection/database/mfa_pane';
import * as i18n from '../../i18n';
import { cancelMFALogin, logIn } from '../../connection/database/actions';
import { hasScreen } from '../../connection/database/index';
import { renderSignedInConfirmation } from '../../core/signed_in_confirmation';

const Component = ({i18n, model}) => {

return <MFAPane
mfaInputPlaceholder={i18n.str("mfaInputPlaceholder")}
i18n={i18n}
instructions={i18n.str("mfaLoginInstructions")}
lock={model}
title={i18n.str("mfaLoginTitle")}
/>;
};

export default class MFALoginScreen extends Screen {

constructor() {
super("mfa.mfaCode");
}

renderAuxiliaryPane(lock) {
return renderSignedInConfirmation(lock);
}

submitButtonLabel(m) {
return i18n.str(m, ["mfaSubmitLabel"]);
}

submitHandler(m) {
return (id) => logIn(id, true);
}

render() {
return Component;
}

backHandler(m) {
return hasScreen(m, "login") ? cancelMFALogin : undefined;
}
}
6 changes: 6 additions & 0 deletions src/field/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ export function username(m) {
return getFieldValue(m, "username");
}

// mfa_code

export function mfaCode(m) {
return getFieldValue(m, "mfa_code");
}

// select field options

export function isSelecting(m) {
Expand Down
37 changes: 37 additions & 0 deletions src/field/mfa-code/mfa_code_pane.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import MFACodeInput from '../../ui/input/mfa_code_input';
import * as c from '../index';
import { swap, updateEntity } from '../../store/index';
import * as l from '../../core/index';
import { setMFACode, getMFACodeValidation } from '../mfa_code';

export default class MFACodePane extends React.Component {

handleChange(e) {
const { lock } = this.props;
swap(updateEntity, "lock", l.id(lock), setMFACode, e.target.value);
}

render() {
const { i18n, lock, placeholder } = this.props;

return (
<MFACodeInput
value={c.getFieldValue(lock, "mfa_code")}
invalidHint={i18n.str("mfaCodeErrorHint", getMFACodeValidation().length)}
isValid={!c.isFieldVisiblyInvalid(lock, "mfa_code")}
onChange={::this.handleChange}
placeholder={placeholder}
disabled={l.submitting(lock)}
/>
);
}

}

MFACodePane.propTypes = {
i18n: React.PropTypes.object.isRequired,
lock: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func,
placeholder: React.PropTypes.string.isRequired
};
34 changes: 34 additions & 0 deletions src/field/mfa_code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { setField } from './index';
import { validateEmail } from './email';
import { databaseConnection } from '../connection/database';
import trim from 'trim';


const DEFAULT_VALIDATION = { mfa_code: { length: 6 } };
const regExp = /^[0-9]+$/;

function validateMFACode(str, settings = DEFAULT_VALIDATION.mfa_code) {
const value = trim(str);

// check min value matched
if (value.length < settings.length) {
return false;
}

// check max value matched
if (value.length > settings.length) {
return false;
}

// check allowed characters matched
const result = regExp.exec(value);
return result && result[0];
}

export function setMFACode(m, str) {
return setField(m, "mfa_code", str, validateMFACode);
}

export function getMFACodeValidation(m) {
return DEFAULT_VALIDATION.mfa_code;
}
9 changes: 8 additions & 1 deletion src/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export default {
"lock.network": "We could not reach the server. Please check your connection and try again.",
"lock.popup_closed": "Popup window closed. Try again.",
"lock.unauthorized": "Permissions were not granted. Try again.",
"lock.mfa_registration_required": "Multifactor authentication is required but your device is not enrolled. Please enroll it before moving on.",
"lock.mfa_invalid_code": "Wrong code. Please try again.",
"password_change_required": "You need to update your password because this is the first time you are logging in, or because your password has expired.", // TODO: verify error code
"password_leaked": "This login has been blocked because your password has been leaked in another website. We’ve sent you an email with instructions on how to unblock it.",
"too_many_attempts": "Your account has been blocked after multiple consecutive login attempts."
Expand Down Expand Up @@ -98,5 +100,10 @@ export default {
title: "Auth0",
welcome: "Welcome %s!",
windowsAuthInstructions: "You are connected from your corporate network&hellip;",
windowsAuthLabel: "Windows Authentication"
windowsAuthLabel: "Windows Authentication",
mfaInputPlaceholder: "Code",
mfaLoginTitle: "2-Step Verification",
mfaLoginInstructions: "Please enter the verification code generated by your mobile application.",
mfaSubmitLabel: "Log In",
mfaCodeErrorHint: "Use %d numbers"
};
11 changes: 9 additions & 2 deletions src/i18n/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export default {
"lock.unauthorized": "Acceso denegado. Por favor, intente nuevamente.",
"password_change_required": "Debe actualizar su contraseña porque es la primera vez que ingresa o porque la contraseña está vencida.",
"password_leaked": "Este intento ha sido bloqueado ya que usted utilizó la misma contraseña para registrarse en otra aplicación que tuvo una filtración reciente. Hemos enviado un email con las instrucciones.",
"too_many_attempts": "Su cuenta ha sido bloqueada luego de múltiples intentos de inicio de sesión consecutivos."
"too_many_attempts": "Su cuenta ha sido bloqueada luego de múltiples intentos de inicio de sesión consecutivos.",
"lock.mfa_registration_required": "Por favor enrole su dispositivo antes de continuar con el segundo factor.",
"lock.mfa_invalid_code": "Código incorrecto. Por favor vuelva a intentarlo.",
},
passwordless: {
"bad.email": "Correo inválido",
Expand Down Expand Up @@ -98,5 +100,10 @@ export default {
title: "Auth0",
welcome: "Bienvenido %s!",
windowsAuthInstructions: "Usted se encuentra conectado desde su red corporativa&hellip;",
windowsAuthLabel: "Autenticación de Windows"
windowsAuthLabel: "Autenticación de Windows",
mfaInputPlaceholder: "Código",
mfaLoginTitle: "Segundo Factor",
mfaLoginInstructions: "Por favor ingrese el código de verificación generado por su aplicación móvil.",
mfaSubmitLabel: "Enviar",
mfaCodeErrorHint: "%d números"
};
Loading

1 comment on commit f82d025

@narwajea
Copy link

Choose a reason for hiding this comment

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

Why just update src/i18n/en.js & src/i18n/es.js ? I get many warnings in fr locale..

Please sign in to comment.