MM-12976: Moving MFA to Team edition (#9971)

* MM-12976: Moving MFA to Team edition

* Changing the mfa disabled i18n key and string
Этот коммит содержится в:
Jesús Espino
2018-12-12 11:50:19 +01:00
коммит произвёл GitHub
родитель f94567c97b
Коммит a7b6c71421
11 изменённых файлов: 268 добавлений и 88 удалений

146
services/mfa/mfa.go Обычный файл
Просмотреть файл

@@ -0,0 +1,146 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package mfa
import (
b32 "encoding/base32"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/dgryski/dgoogauth"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/services/configservice"
"github.com/mattermost/mattermost-server/store"
"github.com/mattermost/rsc/qr"
)
const (
MFA_SECRET_SIZE = 20
)
type Mfa struct {
ConfigService configservice.ConfigService
Store store.Store
}
func New(configService configservice.ConfigService, store store.Store) Mfa {
return Mfa{configService, store}
}
func (m *Mfa) checkConfig() *model.AppError {
if !*m.ConfigService.Config().ServiceSettings.EnableMultifactorAuthentication {
return model.NewAppError("checkConfig", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
}
return nil
}
func getIssuerFromUrl(uri string) string {
issuer := "Mattermost"
siteUrl := strings.TrimSpace(uri)
if len(siteUrl) > 0 {
siteUrl = strings.TrimPrefix(siteUrl, "https://")
siteUrl = strings.TrimPrefix(siteUrl, "http://")
issuer = strings.TrimPrefix(siteUrl, "www.")
}
return url.QueryEscape(issuer)
}
func (m *Mfa) GenerateSecret(user *model.User) (string, []byte, *model.AppError) {
if err := m.checkConfig(); err != nil {
return "", nil, err
}
issuer := getIssuerFromUrl(*m.ConfigService.Config().ServiceSettings.SiteURL)
secret := b32.StdEncoding.EncodeToString([]byte(model.NewRandomString(MFA_SECRET_SIZE)))
authLink := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, user.Email, secret, issuer)
code, err := qr.Encode(authLink, qr.H)
if err != nil {
return "", nil, model.NewAppError("GenerateQrCode", "mfa.generate_qr_code.create_code.app_error", nil, err.Error(), http.StatusInternalServerError)
}
img := code.PNG()
if result := <-m.Store.User().UpdateMfaSecret(user.Id, secret); result.Err != nil {
return "", nil, model.NewAppError("GenerateQrCode", "mfa.generate_qr_code.save_secret.app_error", nil, result.Err.Error(), http.StatusInternalServerError)
}
return secret, img, nil
}
func (m *Mfa) Activate(user *model.User, token string) *model.AppError {
if err := m.checkConfig(); err != nil {
return err
}
otpConfig := &dgoogauth.OTPConfig{
Secret: user.MfaSecret,
WindowSize: 3,
HotpCounter: 0,
}
trimmedToken := strings.TrimSpace(token)
ok, err := otpConfig.Authenticate(trimmedToken)
if err != nil {
return model.NewAppError("Activate", "mfa.activate.authenticate.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if !ok {
return model.NewAppError("Activate", "mfa.activate.bad_token.app_error", nil, "", http.StatusUnauthorized)
}
if result := <-m.Store.User().UpdateMfaActive(user.Id, true); result.Err != nil {
return model.NewAppError("Activate", "mfa.activate.save_active.app_error", nil, result.Err.Error(), http.StatusInternalServerError)
}
return nil
}
func (m *Mfa) Deactivate(userId string) *model.AppError {
if err := m.checkConfig(); err != nil {
return err
}
achan := m.Store.User().UpdateMfaActive(userId, false)
schan := m.Store.User().UpdateMfaSecret(userId, "")
if result := <-achan; result.Err != nil {
return model.NewAppError("Deactivate", "mfa.deactivate.save_active.app_error", nil, result.Err.Error(), http.StatusInternalServerError)
}
if result := <-schan; result.Err != nil {
return model.NewAppError("Deactivate", "mfa.deactivate.save_secret.app_error", nil, result.Err.Error(), http.StatusInternalServerError)
}
return nil
}
func (m *Mfa) ValidateToken(secret, token string) (bool, *model.AppError) {
if err := m.checkConfig(); err != nil {
return false, err
}
otpConfig := &dgoogauth.OTPConfig{
Secret: secret,
WindowSize: 3,
HotpCounter: 0,
}
trimmedToken := strings.TrimSpace(token)
ok, err := otpConfig.Authenticate(trimmedToken)
if err != nil {
return false, model.NewAppError("ValidateToken", "mfa.validate_token.authenticate.app_error", nil, err.Error(), http.StatusBadRequest)
}
return ok, nil
}

73
services/mfa/mfa_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,73 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package mfa
import (
"net/url"
"testing"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/store"
"github.com/mattermost/mattermost-server/store/storetest/mocks"
"github.com/mattermost/mattermost-server/utils/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateSecret(t *testing.T) {
user := &model.User{Id: model.NewId(), Roles: "system_user"}
config := model.Config{}
config.SetDefaults()
config.ServiceSettings.EnableMultifactorAuthentication = model.NewBool(true)
configService := testutils.StaticConfigService{Cfg: &config}
storeMock := mocks.Store{}
userStoreMock := mocks.UserStore{}
userStoreMock.On("UpdateMfaSecret", user.Id, mock.AnythingOfType("string")).Return(func(userId string, secret string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
result.Data = nil
result.Err = nil
})
})
storeMock.On("User").Return(&userStoreMock)
mfa := Mfa{configService, &storeMock}
secret, img, err := mfa.GenerateSecret(user)
require.Nil(t, err)
assert.Len(t, secret, 32)
if len(img) == 0 {
t.Fatal("no image set")
}
config.ServiceSettings.EnableMultifactorAuthentication = model.NewBool(false)
_, _, err = mfa.GenerateSecret(user)
require.NotNil(t, err)
}
func TestGetIssuerFromUrl(t *testing.T) {
cases := []struct {
Input string
Expected string
}{
{"http://somewebsite.com", url.QueryEscape("somewebsite.com")},
{"https://somewebsite.com", url.QueryEscape("somewebsite.com")},
{"https://some.website.com", url.QueryEscape("some.website.com")},
{" https://www.somewebsite.com", url.QueryEscape("somewebsite.com")},
{"http://somewebsite.com/chat", url.QueryEscape("somewebsite.com/chat")},
{"somewebsite.com ", url.QueryEscape("somewebsite.com")},
{"http://localhost:8065", url.QueryEscape("localhost:8065")},
{"", "Mattermost"},
{" ", "Mattermost"},
}
for _, c := range cases {
assert.Equal(t, c.Expected, getIssuerFromUrl(c.Input))
}
}