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
Этот коммит содержится в:
коммит произвёл
enahum
родитель
f0d71d8789
Коммит
30a10d35a8
75
webapp/components/mfa/components/confirm.jsx
Обычный файл
75
webapp/components/mfa/components/confirm.jsx
Обычный файл
@@ -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
Обычный файл
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
Обычный файл
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
|
||||
};
|
||||
Ссылка в новой задаче
Block a user