diff --git a/Makefile b/Makefile index 30d4d9fee3..3505a928de 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 2c00dd4c83..7870393551 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 75e246ab30..5f7cc375d3 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 25f3ed9135..4830d261f8 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 02c6ac9b23..8a361c177f 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 900353701d..304d36a622 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 fd091fd697..a615bd5011 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 3cff93a0f0..617acb7f57 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 = (