summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile5
-rw-r--r--api/user.go15
-rw-r--r--api/user_test.go6
-rw-r--r--einterfaces/mfa.go2
-rw-r--r--model/client.go11
-rw-r--r--webapp/actions/user_actions.jsx17
-rw-r--r--webapp/client/client.jsx9
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx32
-rw-r--r--webapp/i18n/en.json5
-rw-r--r--webapp/tests/client_user.test.jsx13
10 files changed, 90 insertions, 25 deletions
diff --git a/Makefile b/Makefile
index 30d4d9fee..3505a928d 100644
--- a/Makefile
+++ b/Makefile
@@ -204,6 +204,7 @@ ifeq ($(BUILD_ENTERPRISE_READY),true)
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/ldap && ./ldap.test -test.v -test.timeout=120s -test.coverprofile=cldap.out || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/compliance && ./compliance.test -test.v -test.timeout=120s -test.coverprofile=ccompliance.out || exit 1
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/mfa && ./mfa.test -test.v -test.timeout=120s -test.coverprofile=cmfa.out || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/emoji && ./emoji.test -test.v -test.timeout=120s -test.coverprofile=cemoji.out || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/saml && ./saml.test -test.v -test.timeout=60s -test.coverprofile=csaml.out || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -covermode=count -c ./enterprise/cluster && ./cluster.test -test.v -test.timeout=60s -test.coverprofile=ccluster.out || exit 1
@@ -212,14 +213,16 @@ ifeq ($(BUILD_ENTERPRISE_READY),true)
tail -n +2 cldap.out >> ecover.out
tail -n +2 ccompliance.out >> ecover.out
+ tail -n +2 cmfa.out >> ecover.out
tail -n +2 cemoji.out >> ecover.out
tail -n +2 csaml.out >> ecover.out
tail -n +2 ccluster.out >> ecover.out
tail -n +2 caccount_migration.out >> ecover.out
tail -n +2 cwebrtc.out >> ecover.out
- rm -f cldap.out ccompliance.out cemoji.out csaml.out ccluster.out caccount_migration.out cwebrtc.out
+ rm -f cldap.out ccompliance.out cmfa.out cemoji.out csaml.out ccluster.out caccount_migration.out cwebrtc.out
rm -r ldap.test
rm -r compliance.test
+ rm -r mfa.test
rm -r emoji.test
rm -r saml.test
rm -r cluster.test
diff --git a/api/user.go b/api/user.go
index 2c00dd4c8..787039355 100644
--- a/api/user.go
+++ b/api/user.go
@@ -64,7 +64,7 @@ func InitUser() {
BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET")
BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST")
- BaseRoutes.Users.Handle("/generate_mfa_qr", ApiUserRequiredTrustRequester(generateMfaQrCode)).Methods("GET")
+ BaseRoutes.Users.Handle("/generate_mfa_secret", ApiUserRequiredTrustRequester(generateMfaSecret)).Methods("GET")
BaseRoutes.Users.Handle("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST")
BaseRoutes.Users.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).Methods("POST")
@@ -2306,7 +2306,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) {
+func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) {
uchan := Srv.Store.User().Get(c.Session.UserId)
var user *model.User
@@ -2319,22 +2319,25 @@ func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) {
mfaInterface := einterfaces.GetMfaInterface()
if mfaInterface == nil {
- c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.generate_mfa_qr.not_available.app_error", nil, "")
+ c.Err = model.NewLocAppError("generateMfaSecret", "api.user.generate_mfa_qr.not_available.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
- img, err := mfaInterface.GenerateQrCode(user)
+ secret, img, err := mfaInterface.GenerateSecret(user)
if err != nil {
c.Err = err
return
}
- w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
+ resp := map[string]string{}
+ resp["qr_code"] = b64.StdEncoding.EncodeToString(img)
+ resp["secret"] = secret
+
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
- w.Write(img)
+ w.Write([]byte(model.MapToJson(resp)))
}
func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
diff --git a/api/user_test.go b/api/user_test.go
index 75e246ab3..5f7cc375d 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -1687,7 +1687,7 @@ func TestMeInitialLoad(t *testing.T) {
}
-func TestGenerateMfaQrCode(t *testing.T) {
+func TestGenerateMfaSecret(t *testing.T) {
th := Setup()
Client := th.CreateClient()
@@ -1701,13 +1701,13 @@ func TestGenerateMfaQrCode(t *testing.T) {
Client.Logout()
- if _, err := Client.GenerateMfaQrCode(); err == nil {
+ if _, err := Client.GenerateMfaSecret(); err == nil {
t.Fatal("should have failed - not logged in")
}
Client.Login(user.Email, user.Password)
- if _, err := Client.GenerateMfaQrCode(); err == nil {
+ if _, err := Client.GenerateMfaSecret(); err == nil {
t.Fatal("should have failed - not licensed")
}
diff --git a/einterfaces/mfa.go b/einterfaces/mfa.go
index 25f3ed913..4830d261f 100644
--- a/einterfaces/mfa.go
+++ b/einterfaces/mfa.go
@@ -8,7 +8,7 @@ import (
)
type MfaInterface interface {
- GenerateQrCode(user *model.User) ([]byte, *model.AppError)
+ GenerateSecret(user *model.User) (string, []byte, *model.AppError)
Activate(user *model.User, token string) *model.AppError
Deactivate(userId string) *model.AppError
ValidateToken(secret, token string) (bool, *model.AppError)
diff --git a/model/client.go b/model/client.go
index 02c6ac9b2..8a361c177 100644
--- a/model/client.go
+++ b/model/client.go
@@ -696,15 +696,16 @@ func (c *Client) CheckMfa(loginId string) (*Result, *AppError) {
}
}
-// GenerateMfaQrCode returns a QR code imagem containing the secret, to be scanned
-// by a multi-factor authentication mobile application. Must be authenticated.
-func (c *Client) GenerateMfaQrCode() (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/generate_mfa_qr", "", ""); err != nil {
+// GenerateMfaSecret returns a QR code image containing the secret, to be scanned
+// by a multi-factor authentication mobile application. It also returns the secret
+// for manual entry. Must be authenticated.
+func (c *Client) GenerateMfaSecret() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/users/generate_mfa_secret", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), r.Body}, nil
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx
index 900353701..304d36a62 100644
--- a/webapp/actions/user_actions.jsx
+++ b/webapp/actions/user_actions.jsx
@@ -316,3 +316,20 @@ export function autocompleteUsersInTeam(username, success, error) {
}
);
}
+
+export function generateMfaSecret(success, error) {
+ Client.generateMfaSecret(
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'generateMfaSecret');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index fd091fd69..a615bd501 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -990,6 +990,15 @@ export default class Client {
this.track('api', 'api_users_oauth_to_email');
}
+ generateMfaSecret(success, error) {
+ request.
+ get(`${this.getUsersRoute()}/generate_mfa_secret`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'generateMfaSecret', success, error));
+ }
+
revokeSession(altId, success, error) {
request.
post(`${this.getUsersRoute()}/revoke_session`).
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index 3cff93a0f..617acb7f5 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -9,6 +9,8 @@ import ToggleModalButton from '../toggle_modal_button.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import {generateMfaSecret} from 'actions/user_actions.jsx';
+
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -179,7 +181,10 @@ export default class SecurityTab extends React.Component {
showQrCode(e) {
e.preventDefault();
- this.setState({mfaShowQr: true});
+ generateMfaSecret(
+ (data) => this.setState({mfaShowQr: true, secret: data.secret, qrCode: data.qr_code}),
+ (err) => this.setState({serverError: err.message})
+ );
}
deauthorizeApp(e) {
@@ -235,19 +240,31 @@ export default class SecurityTab extends React.Component {
content = (
<div key='mfaButton'>
<div className='form-group'>
- <label className='col-sm-5 control-label'>
+ <label className='col-sm-3 control-label'>
<FormattedMessage
id='user.settings.mfa.qrCode'
defaultMessage='Bar Code'
/>
</label>
- <div className='col-sm-7'>
+ <div className='col-sm-5'>
<img
className='qr-code-img'
- src={Client.getUsersRoute() + '/generate_mfa_qr?time=' + this.props.user.update_at}
+ src={'data:image/png;base64,' + this.state.qrCode}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label className='col-sm-3 control-label'>
+ <FormattedMessage
+ id='user.settings.mfa.secret'
+ defaultMessage='Secret'
/>
+ </label>
+ <div className='col-sm-9 padding-top'>
+ {this.state.secret}
</div>
</div>
+ <hr/>
<div className='form-group'>
<label className='col-sm-5 control-label'>
<FormattedMessage
@@ -272,7 +289,7 @@ export default class SecurityTab extends React.Component {
<span>
<FormattedMessage
id='user.settings.mfa.addHelpQr'
- defaultMessage='Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app.'
+ defaultMessage='Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app. If you are unable to scan the code, you can maunally enter the secret provided.'
/>
</span>
);
@@ -299,7 +316,7 @@ export default class SecurityTab extends React.Component {
<span>
<FormattedHTMLMessage
id='user.settings.mfa.addHelp'
- defaultMessage="You can require a smartphone-based token, in addition to your password, to sign into Mattermost.<br/><br/>To enable, 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> for your phone, then<br/><br/>1. Click the <strong>Add MFA to your account</strong> button above.<br/>2. Use Google Authenticator to scan the QR code that appears.<br/>3. Type in the Token generated by Google Authenticator and click <strong>Save</strong>.<br/><br/>When logging in, you will be asked to enter a token from Google Authenticator in addition to your regular credentials."
+ defaultMessage="You can require a smartphone-based token, in addition to your password, to sign into Mattermost.<br/><br/>To enable, 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> for your phone, then<br/><br/>1. Click the <strong>Add MFA to your account</strong> button above.<br/>2. Use Google Authenticator to scan the QR code that appears or type in the secret manually.<br/>3. Type in the Token generated by Google Authenticator and click <strong>Save</strong>.<br/><br/>When logging in, you will be asked to enter a token from Google Authenticator in addition to your regular credentials."
/>
</span>
);
@@ -309,7 +326,7 @@ export default class SecurityTab extends React.Component {
inputs.push(
<div
key='mfaSetting'
- className='form-group'
+ className='padding-top'
>
{content}
</div>
@@ -330,6 +347,7 @@ export default class SecurityTab extends React.Component {
server_error={this.state.serverError}
client_error={this.state.mfaError}
updateSection={updateSectionStatus}
+ width='medium'
/>
);
}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 20f771608..83dccd4a5 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1914,12 +1914,13 @@
"user.settings.languages.change": "Change interface language",
"user.settings.languages.promote": "Select which language Mattermost displays in the user interface.<br /><br />Would like to help with translations? Join the <a href='http://translate.mattermost.com/' target='_blank'>Mattermost Translation Server</a> to contribute.",
"user.settings.mfa.add": "Add MFA to your account",
- "user.settings.mfa.addHelp": "You can require a smartphone-based token, in addition to your password, to sign into Mattermost.<br/><br/>To enable, 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> for your phone, then<br/><br/>1. Click the <strong>Add MFA to your account</strong> button above.<br/>2. Use Google Authenticator to scan the QR code that appears.<br/>3. Type in the Token generated by Google Authenticator and click <strong>Save</strong>.<br/><br/>When logging in, you will be asked to enter a token from Google Authenticator in addition to your regular credentials.",
- "user.settings.mfa.addHelpQr": "Please scan the bar code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app.",
+ "user.settings.mfa.addHelp": "You can require a smartphone-based token, in addition to your password, to sign into Mattermost.<br/><br/>To enable, 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> for your phone, then<br/><br/>1. Click the <strong>Add MFA to your account</strong> button above.<br/>2. Use Google Authenticator to scan the QR code that appears or type in the secret manually.<br/>3. Type in the Token generated by Google Authenticator and click <strong>Save</strong>.<br/><br/>When logging in, you will be asked to enter a token from Google Authenticator in addition to your regular credentials.",
+ "user.settings.mfa.addHelpQr": "Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app. If you are unable to scan the code, you can maunally enter the secret provided.",
"user.settings.mfa.enterToken": "Token (numbers only)",
"user.settings.mfa.qrCode": "Bar Code",
"user.settings.mfa.remove": "Remove MFA from your account",
"user.settings.mfa.removeHelp": "Removing multi-factor authentication means you will no longer require a phone-based passcode to sign-in to your account.",
+ "user.settings.mfa.secret": "Secret",
"user.settings.mfa.title": "Multi-factor Authentication",
"user.settings.modal.advanced": "Advanced",
"user.settings.modal.confirmBtns": "Yes, Discard",
diff --git a/webapp/tests/client_user.test.jsx b/webapp/tests/client_user.test.jsx
index 8c6f0f970..5e70c5c3e 100644
--- a/webapp/tests/client_user.test.jsx
+++ b/webapp/tests/client_user.test.jsx
@@ -391,6 +391,19 @@ describe('Client.User', function() {
});
});
+ it('generateMfaSecret', function(done) {
+ TestHelper.initBasic(() => {
+ TestHelper.basicClient().generateMfaSecret(
+ function() {
+ done(new Error('not enabled'));
+ },
+ function() {
+ done();
+ }
+ );
+ });
+ });
+
it('getSessions', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().getSessions(