EE: PLT-4512 Show secret in addition to QR code when activating MFA (#4427)

* EE: Update MFA to display secret for manual entry

* Width adjustments for secret (#4423)

* Add unit test
Этот коммит содержится в:
Joram Wilander
2016-11-03 10:41:11 -04:00
коммит произвёл Christopher Speller
родитель 5b34ac6e1e
Коммит 0234f793f2
10 изменённых файлов: 90 добавлений и 25 удалений

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

@@ -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/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/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/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/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 $(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 cldap.out >> ecover.out
tail -n +2 ccompliance.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 cemoji.out >> ecover.out
tail -n +2 csaml.out >> ecover.out tail -n +2 csaml.out >> ecover.out
tail -n +2 ccluster.out >> ecover.out tail -n +2 ccluster.out >> ecover.out
tail -n +2 caccount_migration.out >> ecover.out tail -n +2 caccount_migration.out >> ecover.out
tail -n +2 cwebrtc.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 ldap.test
rm -r compliance.test rm -r compliance.test
rm -r mfa.test
rm -r emoji.test rm -r emoji.test
rm -r saml.test rm -r saml.test
rm -r cluster.test rm -r cluster.test

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

@@ -64,7 +64,7 @@ func InitUser() {
BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET") BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET")
BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST") 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("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST")
BaseRoutes.Users.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).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) uchan := Srv.Store.User().Get(c.Session.UserId)
var user *model.User var user *model.User
@@ -2319,22 +2319,25 @@ func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) {
mfaInterface := einterfaces.GetMfaInterface() mfaInterface := einterfaces.GetMfaInterface()
if mfaInterface == nil { 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 c.Err.StatusCode = http.StatusNotImplemented
return return
} }
img, err := mfaInterface.GenerateQrCode(user) secret, img, err := mfaInterface.GenerateSecret(user)
if err != nil { if err != nil {
c.Err = err c.Err = err
return 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("Cache-Control", "no-cache")
w.Header().Set("Pragma", "no-cache") w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0") w.Header().Set("Expires", "0")
w.Write(img) w.Write([]byte(model.MapToJson(resp)))
} }
func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {

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

@@ -1687,7 +1687,7 @@ func TestMeInitialLoad(t *testing.T) {
} }
func TestGenerateMfaQrCode(t *testing.T) { func TestGenerateMfaSecret(t *testing.T) {
th := Setup() th := Setup()
Client := th.CreateClient() Client := th.CreateClient()
@@ -1701,13 +1701,13 @@ func TestGenerateMfaQrCode(t *testing.T) {
Client.Logout() Client.Logout()
if _, err := Client.GenerateMfaQrCode(); err == nil { if _, err := Client.GenerateMfaSecret(); err == nil {
t.Fatal("should have failed - not logged in") t.Fatal("should have failed - not logged in")
} }
Client.Login(user.Email, user.Password) 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") t.Fatal("should have failed - not licensed")
} }

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

@@ -8,7 +8,7 @@ import (
) )
type MfaInterface interface { 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 Activate(user *model.User, token string) *model.AppError
Deactivate(userId string) *model.AppError Deactivate(userId string) *model.AppError
ValidateToken(secret, token string) (bool, *model.AppError) ValidateToken(secret, token string) (bool, *model.AppError)

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

@@ -696,15 +696,16 @@ func (c *Client) CheckMfa(loginId string) (*Result, *AppError) {
} }
} }
// GenerateMfaQrCode returns a QR code imagem containing the secret, to be scanned // GenerateMfaSecret returns a QR code image containing the secret, to be scanned
// by a multi-factor authentication mobile application. Must be authenticated. // by a multi-factor authentication mobile application. It also returns the secret
func (c *Client) GenerateMfaQrCode() (*Result, *AppError) { // for manual entry. Must be authenticated.
if r, err := c.DoApiGet("/users/generate_mfa_qr", "", ""); err != nil { func (c *Client) GenerateMfaSecret() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/generate_mfa_secret", "", ""); err != nil {
return nil, err return nil, err
} else { } else {
defer closeBody(r) defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID), 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
} }
} }

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

@@ -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);
}
}
);
}

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

@@ -990,6 +990,15 @@ export default class Client {
this.track('api', 'api_users_oauth_to_email'); 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) { revokeSession(altId, success, error) {
request. request.
post(`${this.getUsersRoute()}/revoke_session`). post(`${this.getUsersRoute()}/revoke_session`).

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

@@ -9,6 +9,8 @@ import ToggleModalButton from '../toggle_modal_button.jsx';
import PreferenceStore from 'stores/preference_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx';
import {generateMfaSecret} from 'actions/user_actions.jsx';
import Client from 'client/web_client.jsx'; import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
@@ -179,7 +181,10 @@ export default class SecurityTab extends React.Component {
showQrCode(e) { showQrCode(e) {
e.preventDefault(); 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) { deauthorizeApp(e) {
@@ -235,19 +240,31 @@ export default class SecurityTab extends React.Component {
content = ( content = (
<div key='mfaButton'> <div key='mfaButton'>
<div className='form-group'> <div className='form-group'>
<label className='col-sm-5 control-label'> <label className='col-sm-3 control-label'>
<FormattedMessage <FormattedMessage
id='user.settings.mfa.qrCode' id='user.settings.mfa.qrCode'
defaultMessage='Bar Code' defaultMessage='Bar Code'
/> />
</label> </label>
<div className='col-sm-7'> <div className='col-sm-5'>
<img <img
className='qr-code-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> </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'> <div className='form-group'>
<label className='col-sm-5 control-label'> <label className='col-sm-5 control-label'>
<FormattedMessage <FormattedMessage
@@ -272,7 +289,7 @@ export default class SecurityTab extends React.Component {
<span> <span>
<FormattedMessage <FormattedMessage
id='user.settings.mfa.addHelpQr' 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> </span>
); );
@@ -299,7 +316,7 @@ export default class SecurityTab extends React.Component {
<span> <span>
<FormattedHTMLMessage <FormattedHTMLMessage
id='user.settings.mfa.addHelp' 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> </span>
); );
@@ -309,7 +326,7 @@ export default class SecurityTab extends React.Component {
inputs.push( inputs.push(
<div <div
key='mfaSetting' key='mfaSetting'
className='form-group' className='padding-top'
> >
{content} {content}
</div> </div>
@@ -330,6 +347,7 @@ export default class SecurityTab extends React.Component {
server_error={this.state.serverError} server_error={this.state.serverError}
client_error={this.state.mfaError} client_error={this.state.mfaError}
updateSection={updateSectionStatus} updateSection={updateSectionStatus}
width='medium'
/> />
); );
} }

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

@@ -1914,12 +1914,13 @@
"user.settings.languages.change": "Change interface language", "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.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.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.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 bar code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app.", "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.enterToken": "Token (numbers only)",
"user.settings.mfa.qrCode": "Bar Code", "user.settings.mfa.qrCode": "Bar Code",
"user.settings.mfa.remove": "Remove MFA from your account", "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.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.mfa.title": "Multi-factor Authentication",
"user.settings.modal.advanced": "Advanced", "user.settings.modal.advanced": "Advanced",
"user.settings.modal.confirmBtns": "Yes, Discard", "user.settings.modal.confirmBtns": "Yes, Discard",

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

@@ -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) { it('getSessions', function(done) {
TestHelper.initBasic(() => { TestHelper.initBasic(() => {
TestHelper.basicClient().getSessions( TestHelper.basicClient().getSessions(