Implement some MFA endpoints for APIv4 (#5864)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
a0d5c01dfd
Коммит
58397f853a
22
api/user.go
22
api/user.go
@@ -1234,34 +1234,16 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) {
|
func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
var user *model.User
|
secret, err := app.GenerateMfaSecret(c.Session.UserId)
|
||||||
var err *model.AppError
|
|
||||||
if user, err = app.GetUser(c.Session.UserId); err != nil {
|
|
||||||
c.Err = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mfaInterface := einterfaces.GetMfaInterface()
|
|
||||||
if mfaInterface == nil {
|
|
||||||
c.Err = model.NewLocAppError("generateMfaSecret", "api.user.generate_mfa_qr.not_available.app_error", nil, "")
|
|
||||||
c.Err.StatusCode = http.StatusNotImplemented
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
secret, img, err := mfaInterface.GenerateSecret(user)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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([]byte(model.MapToJson(resp)))
|
w.Write([]byte(secret.ToJson()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
52
api4/user.go
52
api4/user.go
@@ -30,7 +30,6 @@ func InitUser() {
|
|||||||
BaseRoutes.User.Handle("/image", ApiSessionRequired(setProfileImage)).Methods("POST")
|
BaseRoutes.User.Handle("/image", ApiSessionRequired(setProfileImage)).Methods("POST")
|
||||||
BaseRoutes.User.Handle("", ApiSessionRequired(updateUser)).Methods("PUT")
|
BaseRoutes.User.Handle("", ApiSessionRequired(updateUser)).Methods("PUT")
|
||||||
BaseRoutes.User.Handle("/patch", ApiSessionRequired(patchUser)).Methods("PUT")
|
BaseRoutes.User.Handle("/patch", ApiSessionRequired(patchUser)).Methods("PUT")
|
||||||
BaseRoutes.User.Handle("/mfa", ApiSessionRequired(updateUserMfa)).Methods("PUT")
|
|
||||||
BaseRoutes.User.Handle("", ApiSessionRequired(deleteUser)).Methods("DELETE")
|
BaseRoutes.User.Handle("", ApiSessionRequired(deleteUser)).Methods("DELETE")
|
||||||
BaseRoutes.User.Handle("/roles", ApiSessionRequired(updateUserRoles)).Methods("PUT")
|
BaseRoutes.User.Handle("/roles", ApiSessionRequired(updateUserRoles)).Methods("PUT")
|
||||||
BaseRoutes.User.Handle("/password", ApiSessionRequired(updatePassword)).Methods("PUT")
|
BaseRoutes.User.Handle("/password", ApiSessionRequired(updatePassword)).Methods("PUT")
|
||||||
@@ -39,6 +38,10 @@ func InitUser() {
|
|||||||
BaseRoutes.Users.Handle("/email/verify", ApiHandler(verifyUserEmail)).Methods("POST")
|
BaseRoutes.Users.Handle("/email/verify", ApiHandler(verifyUserEmail)).Methods("POST")
|
||||||
BaseRoutes.Users.Handle("/email/verify/send", ApiHandler(sendVerificationEmail)).Methods("POST")
|
BaseRoutes.Users.Handle("/email/verify/send", ApiHandler(sendVerificationEmail)).Methods("POST")
|
||||||
|
|
||||||
|
BaseRoutes.Users.Handle("/mfa", ApiHandler(checkUserMfa)).Methods("POST")
|
||||||
|
BaseRoutes.User.Handle("/mfa", ApiSessionRequired(updateUserMfa)).Methods("PUT")
|
||||||
|
BaseRoutes.User.Handle("/mfa/generate", ApiSessionRequired(generateMfaSecret)).Methods("POST")
|
||||||
|
|
||||||
BaseRoutes.Users.Handle("/login", ApiHandler(login)).Methods("POST")
|
BaseRoutes.Users.Handle("/login", ApiHandler(login)).Methods("POST")
|
||||||
BaseRoutes.Users.Handle("/logout", ApiHandler(logout)).Methods("POST")
|
BaseRoutes.Users.Handle("/logout", ApiHandler(logout)).Methods("POST")
|
||||||
|
|
||||||
@@ -554,6 +557,30 @@ func updateUserRoles(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
ReturnStatusOK(w)
|
ReturnStatusOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
props := model.MapFromJson(r.Body)
|
||||||
|
|
||||||
|
loginId := props["login_id"]
|
||||||
|
if len(loginId) == 0 {
|
||||||
|
c.SetInvalidParam("login_id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]interface{}{}
|
||||||
|
resp["mfa_required"] = false
|
||||||
|
|
||||||
|
if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
|
||||||
|
w.Write([]byte(model.StringInterfaceToJson(resp)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user, err := app.GetUserForLogin(loginId, false); err == nil {
|
||||||
|
resp["mfa_required"] = user.MfaActive
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(model.StringInterfaceToJson(resp)))
|
||||||
|
}
|
||||||
|
|
||||||
func updateUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
func updateUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
c.RequireUserId()
|
c.RequireUserId()
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
@@ -593,6 +620,29 @@ func updateUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
ReturnStatusOK(w)
|
ReturnStatusOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.RequireUserId()
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !app.SessionHasPermissionToUser(c.Session, c.Params.UserId) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := app.GenerateMfaSecret(c.Params.UserId)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Pragma", "no-cache")
|
||||||
|
w.Header().Set("Expires", "0")
|
||||||
|
w.Write([]byte(secret.ToJson()))
|
||||||
|
}
|
||||||
|
|
||||||
func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
|
func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
c.RequireUserId()
|
c.RequireUserId()
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
|
|||||||
@@ -1009,6 +1009,86 @@ func TestGetUsersNotInChannel(t *testing.T) {
|
|||||||
CheckNotImplementedStatus(t, resp)
|
CheckNotImplementedStatus(t, resp)
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
func TestCheckUserMfa(t *testing.T) {
|
||||||
|
th := Setup().InitBasic().InitSystemAdmin()
|
||||||
|
defer TearDown()
|
||||||
|
Client := th.Client
|
||||||
|
|
||||||
|
required, resp := Client.CheckUserMfa(th.BasicUser.Email)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
if required {
|
||||||
|
t.Fatal("should be false - mfa not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, resp = Client.CheckUserMfa("")
|
||||||
|
CheckBadRequestStatus(t, resp)
|
||||||
|
|
||||||
|
Client.Logout()
|
||||||
|
|
||||||
|
required, resp = Client.CheckUserMfa(th.BasicUser.Email)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
if required {
|
||||||
|
t.Fatal("should be false - mfa not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
isLicensed := utils.IsLicensed
|
||||||
|
license := utils.License
|
||||||
|
enableMfa := *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication
|
||||||
|
defer func() {
|
||||||
|
utils.IsLicensed = isLicensed
|
||||||
|
utils.License = license
|
||||||
|
*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = enableMfa
|
||||||
|
}()
|
||||||
|
utils.IsLicensed = true
|
||||||
|
utils.License = &model.License{Features: &model.Features{}}
|
||||||
|
utils.License.Features.SetDefaults()
|
||||||
|
*utils.License.Features.MFA = true
|
||||||
|
*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = true
|
||||||
|
|
||||||
|
th.LoginBasic()
|
||||||
|
|
||||||
|
required, resp = Client.CheckUserMfa(th.BasicUser.Email)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
if required {
|
||||||
|
t.Fatal("should be false - mfa not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.Logout()
|
||||||
|
|
||||||
|
required, resp = Client.CheckUserMfa(th.BasicUser.Email)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
if required {
|
||||||
|
t.Fatal("should be false - mfa not active")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMfaSecret(t *testing.T) {
|
||||||
|
th := Setup().InitBasic().InitSystemAdmin()
|
||||||
|
defer TearDown()
|
||||||
|
Client := th.Client
|
||||||
|
|
||||||
|
_, resp := Client.GenerateMfaSecret(th.BasicUser.Id)
|
||||||
|
CheckNotImplementedStatus(t, resp)
|
||||||
|
|
||||||
|
_, resp = Client.GenerateMfaSecret("junk")
|
||||||
|
CheckBadRequestStatus(t, resp)
|
||||||
|
|
||||||
|
_, resp = Client.GenerateMfaSecret(model.NewId())
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
|
||||||
|
Client.Logout()
|
||||||
|
|
||||||
|
_, resp = Client.GenerateMfaSecret(th.BasicUser.Id)
|
||||||
|
CheckUnauthorizedStatus(t, resp)
|
||||||
|
|
||||||
|
_, resp = th.SystemAdminClient.GenerateMfaSecret(th.BasicUser.Id)
|
||||||
|
CheckNotImplementedStatus(t, resp)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateUserPassword(t *testing.T) {
|
func TestUpdateUserPassword(t *testing.T) {
|
||||||
th := Setup().InitBasic().InitSystemAdmin()
|
th := Setup().InitBasic().InitSystemAdmin()
|
||||||
defer TearDown()
|
defer TearDown()
|
||||||
|
|||||||
22
app/user.go
22
app/user.go
@@ -5,6 +5,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
b64 "encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"image"
|
"image"
|
||||||
@@ -554,6 +555,27 @@ func GetUsersByIds(userIds []string, asAdmin bool) ([]*model.User, *model.AppErr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateMfaSecret(userId string) (*model.MfaSecret, *model.AppError) {
|
||||||
|
mfaInterface := einterfaces.GetMfaInterface()
|
||||||
|
if mfaInterface == nil {
|
||||||
|
return nil, model.NewAppError("generateMfaSecret", "api.user.generate_mfa_qr.not_available.app_error", nil, "", http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
var user *model.User
|
||||||
|
var err *model.AppError
|
||||||
|
if user, err = GetUser(userId); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, img, err := mfaInterface.GenerateSecret(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaSecret := &model.MfaSecret{Secret: secret, QRCode: b64.StdEncoding.EncodeToString(img)}
|
||||||
|
return mfaSecret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ActivateMfa(userId, token string) *model.AppError {
|
func ActivateMfa(userId, token string) *model.AppError {
|
||||||
mfaInterface := einterfaces.GetMfaInterface()
|
mfaInterface := einterfaces.GetMfaInterface()
|
||||||
if mfaInterface == nil {
|
if mfaInterface == nil {
|
||||||
|
|||||||
@@ -545,6 +545,36 @@ func (c *Client4) UpdateUserMfa(userId, code string, activate bool) (bool, *Resp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckUserMfa checks whether a user has MFA active on their account or not based on the
|
||||||
|
// provided login id.
|
||||||
|
func (c *Client4) CheckUserMfa(loginId string) (bool, *Response) {
|
||||||
|
requestBody := make(map[string]interface{})
|
||||||
|
requestBody["login_id"] = loginId
|
||||||
|
|
||||||
|
if r, err := c.DoApiPost(c.GetUsersRoute()+"/mfa", StringInterfaceToJson(requestBody)); err != nil {
|
||||||
|
return false, &Response{StatusCode: r.StatusCode, Error: err}
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
data := StringInterfaceFromJson(r.Body)
|
||||||
|
if mfaRequired, ok := data["mfa_required"].(bool); !ok {
|
||||||
|
return false, BuildResponse(r)
|
||||||
|
} else {
|
||||||
|
return mfaRequired, BuildResponse(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateMfaSecret will generate a new MFA secret for a user and return it as a string and
|
||||||
|
// as a base64 encoded image QR code.
|
||||||
|
func (c *Client4) GenerateMfaSecret(userId string) (*MfaSecret, *Response) {
|
||||||
|
if r, err := c.DoApiPost(c.GetUserRoute(userId)+"/mfa/generate", ""); err != nil {
|
||||||
|
return nil, &Response{StatusCode: r.StatusCode, Error: err}
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
return MfaSecretFromJson(r.Body), BuildResponse(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateUserPassword updates a user's password. Must be logged in as the user or be a system administrator.
|
// UpdateUserPassword updates a user's password. Must be logged in as the user or be a system administrator.
|
||||||
func (c *Client4) UpdateUserPassword(userId, currentPassword, newPassword string) (bool, *Response) {
|
func (c *Client4) UpdateUserPassword(userId, currentPassword, newPassword string) (bool, *Response) {
|
||||||
requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword}
|
requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword}
|
||||||
|
|||||||
34
model/mfa_secret.go
Обычный файл
34
model/mfa_secret.go
Обычный файл
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MfaSecret struct {
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
QRCode string `json:"qr_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *MfaSecret) ToJson() string {
|
||||||
|
b, err := json.Marshal(me)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MfaSecretFromJson(data io.Reader) *MfaSecret {
|
||||||
|
decoder := json.NewDecoder(data)
|
||||||
|
var me MfaSecret
|
||||||
|
err := decoder.Decode(&me)
|
||||||
|
if err == nil {
|
||||||
|
return &me
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
19
model/mfa_secret_test.go
Обычный файл
19
model/mfa_secret_test.go
Обычный файл
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMfaSecretJson(t *testing.T) {
|
||||||
|
secret := MfaSecret{Secret: NewId(), QRCode: NewId()}
|
||||||
|
json := secret.ToJson()
|
||||||
|
result := MfaSecretFromJson(strings.NewReader(json))
|
||||||
|
|
||||||
|
if secret.Secret != result.Secret {
|
||||||
|
t.Fatal("Secrets do not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user