PLT-4767 Implement MFA Enforcement (#4662)

* Create MFA setup page and remove MFA setup from account settings modal

* Add enforce MFA to system console and force redirect

* Lockdown mfa required API routes, add localization, other changes

* Minor fixes

* Fix typo

* Fix some unit tests

* Fix more unit tests

* Minor fix

* Updating UI for MFA screen (#4670)

* Updating UI for MFA screen

* Updating styles for MFA page

* Add the ability to switch between email/sso with MFA enabled

* Added mfa change email

* Minor UI updates for MFA enforcement

* Fix unit test

* Fix client unit test

* Allow switching email to ldap and back when MFA is enabled

* Fix unit test

* Revert config.json
Этот коммит содержится в:
Joram Wilander
2016-12-12 08:16:10 -05:00
коммит произвёл enahum
родитель f0d71d8789
Коммит 30a10d35a8
34 изменённых файлов: 1001 добавлений и 231 удалений

Просмотреть файл

@@ -0,0 +1,75 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import Constants from 'utils/constants.jsx';
const KeyCodes = Constants.KeyCodes;
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
export default class Confirm extends React.Component {
constructor(props) {
super(props);
this.onKeyPress = this.onKeyPress.bind(this);
}
componentDidMount() {
document.body.addEventListener('keydown', this.onKeyPress);
}
componentWillUnmount() {
document.body.removeEventListener('keydown', this.onKeyPress);
}
submit(e) {
e.preventDefault();
browserHistory.push('/');
}
onKeyPress(e) {
if (e.which === KeyCodes.ENTER) {
this.submit(e);
}
}
render() {
return (
<div>
<form
onSubmit={this.submit}
onKeyPress={this.onKeyPress}
className='form-group'
>
<p>
<FormattedHTMLMessage
id='mfa.confirm.complete'
defaultMessage='<strong>Set up complete!</strong>'
/>
</p>
<p>
<FormattedMessage
id='mfa.confirm.secure'
defaultMessage='Your account is now secure. Next time you sign in, you will be asked to enter a code from the Google Authenticator app on your phone.'
/>
</p>
<button
type='submit'
className='btn btn-primary'
>
<FormattedMessage
id='mfa.confirm.okay'
defaultMessage='Okay'
/>
</button>
</form>
</div>
);
}
}
Confirm.defaultProps = {
};
Confirm.propTypes = {
};

156
webapp/components/mfa/components/setup.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,156 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {generateMfaSecret, activateMfa} from 'actions/user_actions.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
export default class Setup extends React.Component {
constructor(props) {
super(props);
this.submit = this.submit.bind(this);
this.state = {secret: '', qrCode: ''};
}
componentDidMount() {
const user = UserStore.getCurrentUser();
if (!user || user.mfa_active) {
browserHistory.push('/');
return;
}
generateMfaSecret(
(data) => this.setState({secret: data.secret, qrCode: data.qr_code}),
(err) => this.setState({serverError: err.message})
);
}
submit(e) {
e.preventDefault();
const code = this.refs.code.value.replace(/\s/g, '');
if (!code || code.length === 0) {
this.setState({error: Utils.localizeMessage('mfa.setup.codeError', 'Please enter the code from Google Authenticator.')});
return;
}
this.setState({error: null});
activateMfa(
code,
() => {
browserHistory.push('/mfa/confirm');
},
(err) => {
if (err.id === 'ent.mfa.activate.authenticate.app_error') {
this.setState({error: Utils.localizeMessage('mfa.setup.badCode', 'Invalid code. If this issue persists, contact your System Administrator.')});
return;
}
this.setState({error: err.message});
}
);
}
render() {
let formClass = 'form-group';
let errorContent;
if (this.state.error) {
errorContent = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
formClass += ' has-error';
}
let mfaRequired;
if (global.window.mm_config.EnforceMultifactorAuthentication) {
mfaRequired = (
<p>
<FormattedHTMLMessage
id='mfa.setup.required'
defaultMessage='<strong>Multi-factor authentication is required on {siteName}.</strong>'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</p>
);
}
return (
<div>
<form
onSubmit={this.submit}
className={formClass}
>
{mfaRequired}
<p>
<FormattedHTMLMessage
id='mfa.setup.step1'
defaultMessage="<strong>Step 1: </strong>On your phone, download Google Authenticator from <a target='_blank' href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8'>iTunes</a> or <a target='_blank' href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en'>Google Play</a>"
/>
</p>
<p>
<FormattedHTMLMessage
id='mfa.setup.step2'
defaultMessage='<strong>Step 2: </strong>Use Google Authenticator to scan this QR code, or manually type in the secret key'
/>
</p>
<div className='form-group'>
<div className='col-sm-12'>
<img
style={{maxHeight: 170}}
src={'data:image/png;base64,' + this.state.qrCode}
/>
</div>
</div>
<br/>
<div className='form-group'>
<p className='col-sm-12'>
<FormattedMessage
id='mfa.setup.secret'
defaultMessage='Secret: {secret}'
values={{
secret: this.state.secret
}}
/>
</p>
</div>
<p>
<FormattedHTMLMessage
id='mfa.setup.step3'
defaultMessage='<strong>Step 3: </strong>Enter the code generated by Google Authenticator'
/>
</p>
<p>
<input
ref='code'
className='form-control'
placeholder={Utils.localizeMessage('mfa.setup.code', 'MFA Code')}
autoFocus={true}
/>
</p>
{errorContent}
<button
type='submit'
className='btn btn-primary'
>
<FormattedMessage
id='mfa.setup.save'
defaultMessage='Save'
/>
</button>
</form>
</div>
);
}
}
Setup.defaultProps = {
};
Setup.propTypes = {
};

66
webapp/components/mfa/mfa_controller.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,66 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router/es6';
import logoImage from 'images/logo.png';
export default class MFAController extends React.Component {
componentDidMount() {
if (window.mm_license.MFA !== 'true' || window.mm_config.EnableMultifactorAuthentication !== 'true') {
browserHistory.push('/');
}
}
render() {
let backButton;
if (window.mm_config.EnforceMultifactorAuthentication !== 'true') {
backButton = (
<div className='signup-header'>
<Link to='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</Link>
</div>
);
}
return (
<div className='inner-wrap sticky'>
<div className='content'>
<div>
{backButton}
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>
<FormattedMessage
id='mfa.setupTitle'
defaultMessage='Multi-factor Authentication Setup'
/>
</h3>
<img
className='signup-team-logo'
src={logoImage}
/>
<div id='mfa'>
{React.cloneElement(this.props.children, {})}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}
MFAController.defaultProps = {
};
MFAController.propTypes = {
location: React.PropTypes.object.isRequired,
children: React.PropTypes.node
};