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

MFA when using oauth/ro endpoint #628

Merged
merged 1 commit into from
Oct 17, 2016
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
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 @@ -141,19 +141,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 @@ -168,15 +169,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 @@ -381,6 +381,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.",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure what to say in this case; we don't support enrollment. "Enrollment not supported"?

"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