diff options
author | Christopher Speller <crspeller@gmail.com> | 2016-03-30 14:33:32 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-03-30 14:33:32 -0400 |
commit | 74d2c5b6ceb507e11032c79252f1778e7512d612 (patch) | |
tree | e2addd98f0a14cdf54fce337bfc9d732de6f0f04 /webapp/components/login | |
parent | 6cad40ecf41740ae57a14df12dc363adafb49d18 (diff) | |
parent | f9a3a4b3949dddecae413b97904c895b2cd887bf (diff) | |
download | chat-74d2c5b6ceb507e11032c79252f1778e7512d612.tar.gz chat-74d2c5b6ceb507e11032c79252f1778e7512d612.tar.bz2 chat-74d2c5b6ceb507e11032c79252f1778e7512d612.zip |
Merge pull request #2518 from mattermost/plt-2187
PLT-2187 Add MFA functionality
Diffstat (limited to 'webapp/components/login')
-rw-r--r-- | webapp/components/login/components/login_email.jsx | 121 | ||||
-rw-r--r-- | webapp/components/login/components/login_ldap.jsx | 101 | ||||
-rw-r--r-- | webapp/components/login/components/login_mfa.jsx | 92 | ||||
-rw-r--r-- | webapp/components/login/components/login_username.jsx | 121 | ||||
-rw-r--r-- | webapp/components/login/login.jsx | 419 |
5 files changed, 854 insertions, 0 deletions
diff --git a/webapp/components/login/components/login_email.jsx b/webapp/components/login/components/login_email.jsx new file mode 100644 index 000000000..b1f484c08 --- /dev/null +++ b/webapp/components/login/components/login_email.jsx @@ -0,0 +1,121 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/utils.jsx'; +import UserStore from 'stores/user_store.jsx'; +import Constants from 'utils/constants.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class LoginEmail extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + serverError: props.serverError + }; + } + componentWillReceiveProps(nextProps) { + this.setState({serverError: nextProps.serverError}); + } + handleSubmit(e) { + e.preventDefault(); + var state = {}; + + const email = this.refs.email.value.trim(); + if (!email) { + state.serverError = Utils.localizeMessage('login_email.emailReq', 'An email is required'); + this.setState(state); + return; + } + + const password = this.refs.password.value.trim(); + if (!password) { + state.serverError = Utils.localizeMessage('login_email.pwdReq', 'A password is required'); + this.setState(state); + return; + } + + state.serverError = ''; + this.setState(state); + + this.props.submit(Constants.EMAIL_SERVICE, email, password); + } + render() { + let serverError; + let errorClass = ''; + if (this.state.serverError) { + serverError = <label className='control-label'>{this.state.serverError}</label>; + errorClass = ' has-error'; + } + + let priorEmail = UserStore.getLastEmail(); + let focusEmail = false; + let focusPassword = false; + if (priorEmail === '') { + focusEmail = true; + } else { + focusPassword = true; + } + + const emailParam = Utils.getUrlParameter('email'); + if (emailParam) { + priorEmail = decodeURIComponent(emailParam); + } + + return ( + <form onSubmit={this.handleSubmit}> + <div className='signup__email-container'> + <div className={'form-group' + errorClass}> + {serverError} + </div> + <div className={'form-group' + errorClass}> + <input + autoFocus={focusEmail} + type='email' + className='form-control' + name='email' + defaultValue={priorEmail} + ref='email' + placeholder={Utils.localizeMessage('login_email.email', 'Email')} + spellCheck='false' + /> + </div> + <div className={'form-group' + errorClass}> + <input + autoFocus={focusPassword} + type='password' + className='form-control' + name='password' + ref='password' + placeholder={Utils.localizeMessage('login_email.pwd', 'Password')} + spellCheck='false' + /> + </div> + <div className='form-group'> + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='login_email.signin' + defaultMessage='Sign in' + /> + </button> + </div> + </div> + </form> + ); + } +} +LoginEmail.defaultProps = { +}; + +LoginEmail.propTypes = { + submit: React.PropTypes.func.isRequired, + serverError: React.PropTypes.string +}; diff --git a/webapp/components/login/components/login_ldap.jsx b/webapp/components/login/components/login_ldap.jsx new file mode 100644 index 000000000..a2013710f --- /dev/null +++ b/webapp/components/login/components/login_ldap.jsx @@ -0,0 +1,101 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class LoginLdap extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + serverError: props.serverError + }; + } + componentWillReceiveProps(nextProps) { + this.setState({serverError: nextProps.serverError}); + } + handleSubmit(e) { + e.preventDefault(); + const state = {}; + + const id = this.refs.id.value.trim(); + if (!id) { + state.serverError = Utils.localizeMessage('login_ldap.idlReq', 'An LDAP ID is required'); + this.setState(state); + return; + } + + const password = this.refs.password.value.trim(); + if (!password) { + state.serverError = Utils.localizeMessage('login_ldap.pwdReq', 'An LDAP password is required'); + this.setState(state); + return; + } + + state.serverError = ''; + this.setState(state); + + this.props.submit(Constants.LDAP_SERVICE, id, password); + } + render() { + let serverError; + let errorClass = ''; + if (this.state.serverError) { + serverError = <label className='control-label'>{this.state.serverError}</label>; + errorClass = ' has-error'; + } + + return ( + <form onSubmit={this.handleSubmit}> + <div className='signup__email-container'> + <div className={'form-group' + errorClass}> + {serverError} + </div> + <div className={'form-group' + errorClass}> + <input + autoFocus={true} + className='form-control' + ref='id' + placeholder={Utils.localizeMessage('login_ldap.username', 'LDAP Username')} + spellCheck='false' + /> + </div> + <div className={'form-group' + errorClass}> + <input + type='password' + className='form-control' + ref='password' + placeholder={Utils.localizeMessage('login_ldap.pwd', 'LDAP Password')} + spellCheck='false' + /> + </div> + <div className='form-group'> + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='login_ldap.signin' + defaultMessage='Sign in' + /> + </button> + </div> + </div> + </form> + ); + } +} +LoginLdap.defaultProps = { +}; + +LoginLdap.propTypes = { + serverError: React.PropTypes.string, + submit: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/login/components/login_mfa.jsx b/webapp/components/login/components/login_mfa.jsx new file mode 100644 index 000000000..f8ebf1e82 --- /dev/null +++ b/webapp/components/login/components/login_mfa.jsx @@ -0,0 +1,92 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class LoginMfa extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + serverError: '' + }; + } + handleSubmit(e) { + e.preventDefault(); + const state = {}; + + const token = this.refs.token.value.trim(); + if (!token) { + state.serverError = Utils.localizeMessage('login_mfa.tokenReq', 'Please enter an MFA token'); + this.setState(state); + return; + } + + state.serverError = ''; + this.setState(state); + + this.props.submit(this.props.method, this.props.loginId, this.props.password, token); + } + render() { + let serverError; + let errorClass = ''; + if (this.state.serverError) { + serverError = <label className='control-label'>{this.state.serverError}</label>; + errorClass = ' has-error'; + } + + return ( + <form onSubmit={this.handleSubmit}> + <div className='signup__email-container'> + <p> + <FormattedMessage + id='login_mfa.enterToken' + defaultMessage="To complete the sign in process, please enter a token from your smartphone's authenticator" + /> + </p> + <div className={'form-group' + errorClass}> + {serverError} + </div> + <div className={'form-group' + errorClass}> + <input + type='text' + className='form-control' + name='token' + ref='token' + placeholder={Utils.localizeMessage('login_mfa.token', 'MFA Token')} + spellCheck='false' + autoComplete='off' + autoFocus={true} + /> + </div> + <div className='form-group'> + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='login_mfa.submit' + defaultMessage='Submit' + /> + </button> + </div> + </div> + </form> + ); + } +} +LoginMfa.defaultProps = { +}; + +LoginMfa.propTypes = { + method: React.PropTypes.string.isRequired, + loginId: React.PropTypes.string.isRequired, + password: React.PropTypes.string.isRequired, + submit: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/login/components/login_username.jsx b/webapp/components/login/components/login_username.jsx new file mode 100644 index 000000000..3cb213994 --- /dev/null +++ b/webapp/components/login/components/login_username.jsx @@ -0,0 +1,121 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/utils.jsx'; +import UserStore from 'stores/user_store.jsx'; +import Constants from 'utils/constants.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class LoginUsername extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + serverError: props.serverError + }; + } + componentWillReceiveProps(nextProps) { + this.setState({serverError: nextProps.serverError}); + } + handleSubmit(e) { + e.preventDefault(); + const state = {}; + + const username = this.refs.username.value.trim(); + if (!username) { + state.serverError = Utils.localizeMessage('login_username.usernameReq', 'A username is required'); + this.setState(state); + return; + } + + const password = this.refs.password.value.trim(); + if (!password) { + state.serverError = Utils.localizeMessage('login_username.pwdReq', 'A password is required'); + this.setState(state); + return; + } + + state.serverError = ''; + this.setState(state); + + this.props.submit(Constants.USERNAME_SERVICE, username, password); + } + render() { + let serverError; + let errorClass = ''; + if (this.state.serverError) { + serverError = <label className='control-label'>{this.state.serverError}</label>; + errorClass = ' has-error'; + } + + let priorUsername = UserStore.getLastUsername(); + let focusUsername = false; + let focusPassword = false; + if (priorUsername === '') { + focusUsername = true; + } else { + focusPassword = true; + } + + const emailParam = Utils.getUrlParameter('email'); + if (emailParam) { + priorUsername = decodeURIComponent(emailParam); + } + + return ( + <form onSubmit={this.handleSubmit}> + <div className='signup__email-container'> + <div className={'form-group' + errorClass}> + {serverError} + </div> + <div className={'form-group' + errorClass}> + <input + autoFocus={focusUsername} + type='username' + className='form-control' + name='username' + defaultValue={priorUsername} + ref='username' + placeholder={Utils.localizeMessage('login_username.username', 'Username')} + spellCheck='false' + /> + </div> + <div className={'form-group' + errorClass}> + <input + autoFocus={focusPassword} + type='password' + className='form-control' + name='password' + ref='password' + placeholder={Utils.localizeMessage('login_username.pwd', 'Password')} + spellCheck='false' + /> + </div> + <div className='form-group'> + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='login_username.signin' + defaultMessage='Sign in' + /> + </button> + </div> + </div> + </form> + ); + } +} +LoginUsername.defaultProps = { +}; + +LoginUsername.propTypes = { + serverError: React.PropTypes.string, + submit: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/login/login.jsx b/webapp/components/login/login.jsx new file mode 100644 index 000000000..7b370a939 --- /dev/null +++ b/webapp/components/login/login.jsx @@ -0,0 +1,419 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoginEmail from './components/login_email.jsx'; +import LoginUsername from './components/login_username.jsx'; +import LoginLdap from './components/login_ldap.jsx'; +import LoginMfa from './components/login_mfa.jsx'; + +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import * as Client from 'utils/client.jsx'; +import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; + +import {FormattedMessage} from 'react-intl'; +import {browserHistory, Link} from 'react-router'; + +import React from 'react'; + +export default class Login extends React.Component { + constructor(props) { + super(props); + + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onTeamChange = this.onTeamChange.bind(this); + this.preSubmit = this.preSubmit.bind(this); + this.submit = this.submit.bind(this); + + this.state = this.getStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + Client.getMeLoggedIn((data) => { + if (data && data.logged_in !== 'false') { + browserHistory.push('/' + this.props.params.team + '/channels/town-square'); + } else { + this.setState({doneCheckLogin: true}); //eslint-disable-line react/no-did-mount-set-state + } + }); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + getStateFromStores() { + return { + currentTeam: TeamStore.getByName(this.props.params.team), + doneCheckLogin: false + }; + } + onTeamChange() { + this.setState(this.getStateFromStores()); + } + preSubmit(method, loginId, password) { + if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') { + this.submit(method, loginId, password, ''); + return; + } + + Client.checkMfa(method, this.state.currentTeam.name, loginId, + (data) => { + if (data.mfa_required === 'true') { + this.setState({showMfa: true, method, loginId, password}); + } else { + this.submit(method, loginId, password, ''); + } + }, + (err) => { + if (method === Constants.EMAIL_SERVICE) { + this.setState({serverEmailError: err.message}); + } else if (method === Constants.USERNAME_SERVICE) { + this.setState({serverUsernameError: err.message}); + } else if (method === Constants.LDAP_SERVICE) { + this.setState({serverLdapError: err.message}); + } + } + ); + } + submit(method, loginId, password, token) { + this.setState({showMfa: false, serverEmailError: null, serverUsernameError: null, serverLdapError: null}); + + const team = this.state.currentTeam.name; + + if (method === Constants.EMAIL_SERVICE) { + Client.loginByEmail(team, loginId, password, token, + () => { + UserStore.setLastEmail(loginId); + browserHistory.push('/' + team + '/channels/town-square'); + }, + (err) => { + if (err.id === 'api.user.login.not_verified.app_error') { + browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(loginId)); + return; + } + this.setState({serverEmailError: err.message}); + } + ); + } else if (method === Constants.USERNAME_SERVICE) { + Client.loginByUsername(team, loginId, password, token, + () => { + UserStore.setLastUsername(loginId); + + const redirect = Utils.getUrlParameter('redirect'); + if (redirect) { + browserHistory.push(decodeURIComponent(redirect)); + } else { + browserHistory.push('/' + team + '/channels/town-square'); + } + }, + (err) => { + if (err.id === 'api.user.login.not_verified.app_error') { + this.setState({serverUsernameError: Utils.localizeMessage('login_username.verifyEmailError', 'Please verify your email address. Check your inbox for an email.')}); + } else if (err.id === 'store.sql_user.get_by_username.app_error') { + this.setState({serverUsernameError: Utils.localizeMessage('login_username.userNotFoundError', 'We couldn\'t find an existing account matching your username for this team.')}); + } else { + this.setState({serverUsernameError: err.message}); + } + } + ); + } else if (method === Constants.LDAP_SERVICE) { + Client.loginByLdap(team, loginId, password, token, + () => { + const redirect = Utils.getUrlParameter('redirect'); + if (redirect) { + browserHistory.push(decodeURIComponent(redirect)); + } else { + browserHistory.push('/' + team + '/channels/town-square'); + } + }, + (err) => { + this.setState({serverLdapError: err.message}); + } + ); + } + } + createLoginOptions(currentTeam) { + const extraParam = Utils.getUrlParameter('extra'); + let extraBox = ''; + if (extraParam) { + if (extraParam === Constants.SIGNIN_CHANGE) { + extraBox = ( + <div className='alert alert-success'> + <i className='fa fa-check'/> + <FormattedMessage + id='login.changed' + defaultMessage=' Sign-in method changed successfully' + /> + </div> + ); + } else if (extraParam === Constants.SIGNIN_VERIFIED) { + extraBox = ( + <div className='alert alert-success'> + <i className='fa fa-check'/> + <FormattedMessage + id='login.verified' + defaultMessage=' Email Verified' + /> + </div> + ); + } else if (extraParam === Constants.SESSION_EXPIRED) { + extraBox = ( + <div className='alert alert-warning'> + <i className='fa fa-exclamation-triangle'/> + <FormattedMessage + id='login.session_expired' + defaultMessage=' Your session has expired. Please login again.' + /> + </div> + ); + } + } + + const teamName = currentTeam.name; + const ldapEnabled = global.window.mm_config.EnableLdap === 'true'; + const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true'; + const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true'; + const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true'; + const emailSigninEnabled = global.window.mm_config.EnableSignInWithEmail === 'true'; + + const oauthLogins = []; + if (gitlabSigninEnabled) { + oauthLogins.push( + <Link + className='btn btn-custom-login gitlab' + key='gitlab' + to={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)} + > + <span className='icon'/> + <span> + <FormattedMessage + id='login.gitlab' + defaultMessage='with GitLab' + /> + </span> + </Link> + ); + } + + if (googleSigninEnabled) { + oauthLogins.push( + <Link + className='btn btn-custom-login google' + key='google' + to={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)} + > + <span className='icon'/> + <span> + <FormattedMessage + id='login.google' + defaultMessage='with Google Apps' + /> + </span> + </Link> + ); + } + + let emailLogin; + if (emailSigninEnabled) { + emailLogin = ( + <LoginEmail + teamName={teamName} + serverError={this.state.serverEmailError} + submit={this.preSubmit} + /> + ); + + if (oauthLogins.length > 0) { + emailLogin = ( + <div> + <div className='or__container'> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> + </div> + {emailLogin} + </div> + ); + } + } + + let usernameLogin; + if (usernameSigninEnabled) { + usernameLogin = ( + <LoginUsername + teamName={teamName} + serverError={this.state.serverUsernameError} + submit={this.preSubmit} + /> + ); + + if (emailSigninEnabled || oauthLogins.length > 0) { + usernameLogin = ( + <div> + <div className='or__container'> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> + </div> + {usernameLogin} + </div> + ); + } + } + + let ldapLogin; + if (ldapEnabled) { + ldapLogin = ( + <LoginLdap + teamName={teamName} + serverError={this.state.serverLdapError} + submit={this.preSubmit} + /> + ); + + if (emailSigninEnabled || usernameSigninEnabled || oauthLogins.length > 0) { + ldapLogin = ( + <div> + <div className='or__container'> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> + </div> + {ldapLogin} + </div> + ); + } + } + + let userSignUp; + if (currentTeam.allow_open_invite) { + userSignUp = ( + <div> + <span> + <FormattedMessage + id='login.noAccount' + defaultMessage="Don't have an account? " + /> + <Link + to={'/signup_user_complete/?id=' + currentTeam.invite_id} + className='signup-team-login' + > + <FormattedMessage + id='login.create' + defaultMessage='Create one now' + /> + </Link> + </span> + </div> + ); + } + + let forgotPassword; + if (usernameSigninEnabled || emailSigninEnabled) { + forgotPassword = ( + <div className='form-group'> + <Link to={'/' + teamName + '/reset_password'}> + <FormattedMessage + id='login.forgot' + defaultMessage='I forgot my password' + /> + </Link> + </div> + ); + } + + let teamSignUp; + if (global.window.mm_config.EnableTeamCreation === 'true' && !Utils.isMobileApp()) { + teamSignUp = ( + <div className='margin--extra'> + <Link + to='/' + className='signup-team-login' + > + <FormattedMessage + id='login.createTeam' + defaultMessage='Create a new team' + /> + </Link> + </div> + ); + } + + return ( + <div> + {extraBox} + {oauthLogins} + {emailLogin} + {usernameLogin} + {ldapLogin} + {userSignUp} + {forgotPassword} + {teamSignUp} + </div> + ); + } + render() { + const currentTeam = this.state.currentTeam; + if (currentTeam == null || !this.state.doneCheckLogin) { + return <div/>; + } + + let content; + if (this.state.showMfa) { + content = ( + <LoginMfa + method={this.state.method} + loginId={this.state.loginId} + password={this.state.password} + submit={this.submit} + /> + ); + } else { + content = this.createLoginOptions(currentTeam); + } + + return ( + <div> + <div className='signup-header'> + <Link to='/'> + <span className='fa fa-chevron-left'/> + <FormattedMessage + id='web.header.back' + /> + </Link> + </div> + <div className='col-sm-12'> + <div className='signup-team__container'> + <h5 className='margin--less'> + <FormattedMessage + id='login.signTo' + defaultMessage='Sign in to:' + /> + </h5> + <h2 className='signup-team__name'>{currentTeam.display_name}</h2> + <h2 className='signup-team__subdomain'> + <FormattedMessage + id='login.on' + defaultMessage='on {siteName}' + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </h2> + {content} + </div> + </div> + </div> + ); + } +} + +Login.defaultProps = { +}; +Login.propTypes = { + params: React.PropTypes.object.isRequired +}; |