mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Implement some MFA endpoints for APIv4 (#5864)
This commit is contained in:
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) {
|
||||
var user *model.User
|
||||
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)
|
||||
secret, err := app.GenerateMfaSecret(c.Session.UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
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("Pragma", "no-cache")
|
||||
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) {
|
||||
|
||||
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("", ApiSessionRequired(updateUser)).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("/roles", ApiSessionRequired(updateUserRoles)).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/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("/logout", ApiHandler(logout)).Methods("POST")
|
||||
|
||||
@@ -554,6 +557,30 @@ func updateUserRoles(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
@@ -593,6 +620,29 @@ func updateUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
|
||||
@@ -1009,6 +1009,86 @@ func TestGetUsersNotInChannel(t *testing.T) {
|
||||
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) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
defer TearDown()
|
||||
|
||||
22
app/user.go
22
app/user.go
@@ -5,6 +5,7 @@ package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
b64 "encoding/base64"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"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 {
|
||||
mfaInterface := einterfaces.GetMfaInterface()
|
||||
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.
|
||||
func (c *Client4) UpdateUserPassword(userId, currentPassword, newPassword string) (bool, *Response) {
|
||||
requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword}
|
||||
|
||||
34
model/mfa_secret.go
Normal file
34
model/mfa_secret.go
Normal file
@@ -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
Normal file
19
model/mfa_secret_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user