Add MFA functionality
Этот коммит содержится в:
197
api/user.go
197
api/user.go
@@ -53,6 +53,9 @@ func InitUser(r *mux.Router) {
|
||||
sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST")
|
||||
sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST")
|
||||
sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST")
|
||||
sr.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST")
|
||||
sr.Handle("/generate_mfa_qr", ApiUserRequired(generateMfaQrCode)).Methods("GET")
|
||||
sr.Handle("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST")
|
||||
|
||||
sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
|
||||
|
||||
@@ -405,13 +408,14 @@ func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDispl
|
||||
}()
|
||||
}
|
||||
|
||||
func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) *model.User {
|
||||
func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, mfaToken, deviceId string) *model.User {
|
||||
if result := <-Srv.Store.User().Get(userId); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return nil
|
||||
} else {
|
||||
user := result.Data.(*model.User)
|
||||
if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
|
||||
|
||||
if authenticateUserPasswordAndToken(c, user, password, mfaToken) {
|
||||
Login(c, w, r, user, deviceId)
|
||||
return user
|
||||
}
|
||||
@@ -420,7 +424,7 @@ func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, passw
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) *model.User {
|
||||
func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, mfaToken, deviceId string) *model.User {
|
||||
var team *model.Team
|
||||
|
||||
if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
|
||||
@@ -443,7 +447,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
|
||||
if authenticateUserPasswordAndToken(c, user, password, mfaToken) {
|
||||
Login(c, w, r, user, deviceId)
|
||||
return user
|
||||
}
|
||||
@@ -452,7 +456,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, deviceId string) *model.User {
|
||||
func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, mfaToken, deviceId string) *model.User {
|
||||
var team *model.Team
|
||||
|
||||
if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
|
||||
@@ -475,7 +479,7 @@ func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, usernam
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
|
||||
if authenticateUserPasswordAndToken(c, user, password, mfaToken) {
|
||||
Login(c, w, r, user, deviceId)
|
||||
return user
|
||||
}
|
||||
@@ -518,6 +522,10 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st
|
||||
}
|
||||
}
|
||||
|
||||
func authenticateUserPasswordAndToken(c *Context, user *model.User, password string, token string) bool {
|
||||
return checkUserLoginAttempts(c, user) && checkUserMfa(c, user, token) && checkUserPassword(c, user, password)
|
||||
}
|
||||
|
||||
func checkUserLoginAttempts(c *Context, user *model.User) bool {
|
||||
if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts {
|
||||
c.LogAuditWithUserId(user.Id, "fail")
|
||||
@@ -530,7 +538,6 @@ func checkUserLoginAttempts(c *Context, user *model.User) bool {
|
||||
}
|
||||
|
||||
func checkUserPassword(c *Context, user *model.User, password string) bool {
|
||||
|
||||
if !model.ComparePassword(user.Password, password) {
|
||||
c.LogAuditWithUserId(user.Id, "fail")
|
||||
c.Err = model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id)
|
||||
@@ -548,7 +555,29 @@ func checkUserPassword(c *Context, user *model.User, password string) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func checkUserMfa(c *Context, user *model.User, token string) bool {
|
||||
if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
|
||||
return true
|
||||
}
|
||||
|
||||
mfaInterface := einterfaces.GetMfaInterface()
|
||||
if mfaInterface == nil {
|
||||
c.Err = model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return false
|
||||
}
|
||||
|
||||
if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil {
|
||||
c.Err = err
|
||||
return false
|
||||
} else if !ok {
|
||||
c.Err = model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "")
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// User MUST be validated before calling Login
|
||||
@@ -660,11 +689,11 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var user *model.User
|
||||
if len(props["id"]) != 0 {
|
||||
user = LoginById(c, w, r, props["id"], props["password"], props["device_id"])
|
||||
user = LoginById(c, w, r, props["id"], props["password"], props["token"], props["device_id"])
|
||||
} else if len(props["email"]) != 0 && len(props["name"]) != 0 {
|
||||
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"])
|
||||
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["token"], props["device_id"])
|
||||
} else if len(props["username"]) != 0 && len(props["name"]) != 0 {
|
||||
user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["device_id"])
|
||||
user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["token"], props["device_id"])
|
||||
} else {
|
||||
c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
@@ -695,6 +724,7 @@ func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
password := props["password"]
|
||||
id := props["id"]
|
||||
teamName := props["teamName"]
|
||||
mfaToken := props["token"]
|
||||
|
||||
if len(password) == 0 {
|
||||
c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.blank_pwd.app_error", nil, "")
|
||||
@@ -735,6 +765,10 @@ func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !checkUserMfa(c, user, mfaToken) {
|
||||
return
|
||||
}
|
||||
|
||||
// User is authenticated at this point
|
||||
|
||||
Login(c, w, r, user, props["device_id"])
|
||||
@@ -2487,3 +2521,146 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
uchan := Srv.Store.User().Get(c.Session.UserId)
|
||||
tchan := Srv.Store.Team().Get(c.Session.TeamId)
|
||||
|
||||
var user *model.User
|
||||
if result := <-uchan; result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
var team *model.Team
|
||||
if result := <-tchan; result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
mfaInterface := einterfaces.GetMfaInterface()
|
||||
if mfaInterface == nil {
|
||||
c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.generate_mfa_qr.not_available.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
img, err := mfaInterface.GenerateQrCode(team, user)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
|
||||
w.Write(img)
|
||||
}
|
||||
|
||||
func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.StringInterfaceFromJson(r.Body)
|
||||
|
||||
activate, ok := props["activate"].(bool)
|
||||
if !ok {
|
||||
c.SetInvalidParam("updateMfa", "activate")
|
||||
return
|
||||
}
|
||||
|
||||
token := ""
|
||||
if activate {
|
||||
token = props["token"].(string)
|
||||
if len(token) == 0 {
|
||||
c.SetInvalidParam("updateMfa", "token")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mfaInterface := einterfaces.GetMfaInterface()
|
||||
if mfaInterface == nil {
|
||||
c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.update_mfa.not_available.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
if activate {
|
||||
var user *model.User
|
||||
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
if err := mfaInterface.Activate(user, token); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := mfaInterface.Deactivate(c.Session.UserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rdata := map[string]string{}
|
||||
rdata["status"] = "ok"
|
||||
w.Write([]byte(model.MapToJson(rdata)))
|
||||
}
|
||||
|
||||
func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
|
||||
rdata := map[string]string{}
|
||||
rdata["mfa_required"] = "false"
|
||||
w.Write([]byte(model.MapToJson(rdata)))
|
||||
return
|
||||
}
|
||||
|
||||
props := model.MapFromJson(r.Body)
|
||||
|
||||
method := props["method"]
|
||||
if method != model.USER_AUTH_SERVICE_EMAIL &&
|
||||
method != model.USER_AUTH_SERVICE_USERNAME &&
|
||||
method != model.USER_AUTH_SERVICE_LDAP {
|
||||
c.SetInvalidParam("checkMfa", "method")
|
||||
return
|
||||
}
|
||||
|
||||
teamName := props["team_name"]
|
||||
if len(teamName) == 0 {
|
||||
c.SetInvalidParam("checkMfa", "team_name")
|
||||
return
|
||||
}
|
||||
|
||||
loginId := props["login_id"]
|
||||
if len(loginId) == 0 {
|
||||
c.SetInvalidParam("checkMfa", "login_id")
|
||||
return
|
||||
}
|
||||
|
||||
var team *model.Team
|
||||
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
var uchan store.StoreChannel
|
||||
if method == model.USER_AUTH_SERVICE_EMAIL {
|
||||
uchan = Srv.Store.User().GetByEmail(team.Id, loginId)
|
||||
} else if method == model.USER_AUTH_SERVICE_USERNAME {
|
||||
uchan = Srv.Store.User().GetByUsername(team.Id, loginId)
|
||||
} else if method == model.USER_AUTH_SERVICE_LDAP {
|
||||
uchan = Srv.Store.User().GetByAuth(team.Id, loginId, model.USER_AUTH_SERVICE_LDAP)
|
||||
}
|
||||
|
||||
rdata := map[string]string{}
|
||||
if result := <-uchan; result.Err != nil {
|
||||
rdata["mfa_required"] = "false"
|
||||
} else {
|
||||
rdata["mfa_required"] = strconv.FormatBool(result.Data.(*model.User).MfaActive)
|
||||
}
|
||||
w.Write([]byte(model.MapToJson(rdata)))
|
||||
}
|
||||
|
||||
@@ -1411,3 +1411,79 @@ func TestMeLoggedIn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMfaQrCode(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
rteam, _ := Client.CreateTeam(&team)
|
||||
|
||||
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
ruser, _ := Client.CreateUser(&user, "")
|
||||
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
|
||||
|
||||
Client.Logout()
|
||||
|
||||
if _, err := Client.GenerateMfaQrCode(); err == nil {
|
||||
t.Fatal("should have failed - not logged in")
|
||||
}
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, user.Password)
|
||||
|
||||
if _, err := Client.GenerateMfaQrCode(); err == nil {
|
||||
t.Fatal("should have failed - not licensed")
|
||||
}
|
||||
|
||||
// need to add more test cases when license and config can be configured for tests
|
||||
}
|
||||
|
||||
func TestUpdateMfa(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
rteam, _ := Client.CreateTeam(&team)
|
||||
|
||||
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
ruser, _ := Client.CreateUser(&user, "")
|
||||
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
|
||||
|
||||
Client.Logout()
|
||||
|
||||
if _, err := Client.UpdateMfa(true, "123456"); err == nil {
|
||||
t.Fatal("should have failed - not logged in")
|
||||
}
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, user.Password)
|
||||
|
||||
if _, err := Client.UpdateMfa(true, ""); err == nil {
|
||||
t.Fatal("should have failed - no token")
|
||||
}
|
||||
|
||||
if _, err := Client.UpdateMfa(true, "123456"); err == nil {
|
||||
t.Fatal("should have failed - not licensed")
|
||||
}
|
||||
|
||||
// need to add more test cases when license and config can be configured for tests
|
||||
}
|
||||
|
||||
func TestCheckMfa(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
rteam, _ := Client.CreateTeam(&team)
|
||||
|
||||
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
ruser, _ := Client.CreateUser(&user, "")
|
||||
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
|
||||
|
||||
if result, err := Client.CheckMfa(model.USER_AUTH_SERVICE_EMAIL, team.Name, user.Email); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
resp := result.Data.(map[string]string)
|
||||
if resp["mfa_required"] != "false" {
|
||||
t.Fatal("mfa should not be required")
|
||||
}
|
||||
}
|
||||
|
||||
// need to add more test cases when license and config can be configured for tests
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user