PLT-2258 Unified login screen and related APIs (#2820)
* Unified login screen and related APIs * Refactored login API call to be less convoluted * Removed LDAP login prompt from invite process * Fixed existing LDAP users being able to log in if LDAP was configured, but disabled * Gofmt * Future proofed login API * Updated login APIs based on feedback * Added additional auditing to login API * Actually removed loginById
Этот коммит содержится в:
коммит произвёл
Christopher Speller
родитель
e76a30bca0
Коммит
87989b8afd
254
api/user.go
254
api/user.go
@@ -45,7 +45,6 @@ func InitUser() {
|
||||
BaseRoutes.Users.Handle("/reset_password", ApiAppHandler(resetPassword)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/login", ApiAppHandler(login)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/logout", ApiAppHandler(logout)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST")
|
||||
BaseRoutes.Users.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST")
|
||||
@@ -333,7 +332,7 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service
|
||||
}
|
||||
}
|
||||
|
||||
Login(c, w, r, ruser, "")
|
||||
doLogin(c, w, r, ruser, "")
|
||||
if c.Err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -431,116 +430,88 @@ func SendVerifyEmailAndForget(c *Context, userId, userEmail, siteURL string) {
|
||||
func login(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.MapFromJson(r.Body)
|
||||
|
||||
if len(props["password"]) == 0 {
|
||||
id := props["id"]
|
||||
loginId := props["login_id"]
|
||||
password := props["password"]
|
||||
mfaToken := props["token"]
|
||||
deviceId := props["device_id"]
|
||||
|
||||
if len(password) == 0 {
|
||||
c.Err = model.NewLocAppError("login", "api.user.login.blank_pwd.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
if len(props["id"]) != 0 {
|
||||
user = LoginById(c, w, r, props["id"], props["password"], props["token"], props["device_id"])
|
||||
} else if len(props["email"]) != 0 {
|
||||
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["token"], props["device_id"])
|
||||
} else if len(props["username"]) != 0 {
|
||||
user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["token"], props["device_id"])
|
||||
var err *model.AppError
|
||||
|
||||
if len(id) != 0 {
|
||||
c.LogAuditWithUserId(id, "attempt")
|
||||
|
||||
if result := <-Srv.Store.User().Get(id); result.Err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "failure")
|
||||
c.Err = result.Err
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
} else {
|
||||
c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if user, err = getUserForLogin(loginId); err != nil {
|
||||
c.LogAudit("failure")
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
c.LogAuditWithUserId(user.Id, "attempt")
|
||||
}
|
||||
|
||||
// and then authenticate them
|
||||
if user, err = authenticateUser(user, password, mfaToken); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "failure")
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
c.LogAuditWithUserId(user.Id, "success")
|
||||
|
||||
doLogin(c, w, r, user, deviceId)
|
||||
|
||||
user.Sanitize(map[string]bool{})
|
||||
|
||||
if user != nil {
|
||||
user.Sanitize(map[string]bool{})
|
||||
} else {
|
||||
user = &model.User{}
|
||||
}
|
||||
w.Write([]byte(user.ToJson()))
|
||||
}
|
||||
|
||||
func doUserPasswordAuthenticationAndLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, password string, mfaToken string, deviceId string) bool {
|
||||
c.LogAuditWithUserId(user.Id, "attempt")
|
||||
if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "fail")
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return false
|
||||
func getUserForLogin(loginId string) (*model.User, *model.AppError) {
|
||||
ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil
|
||||
|
||||
if result := <-Srv.Store.User().GetForLogin(
|
||||
loginId,
|
||||
*utils.Cfg.EmailSettings.EnableSignInWithUsername,
|
||||
*utils.Cfg.EmailSettings.EnableSignInWithEmail,
|
||||
ldapAvailable,
|
||||
); result.Err != nil {
|
||||
|
||||
if !ldapAvailable {
|
||||
// failed to find user and no LDAP server to fall back on
|
||||
result.Err.StatusCode = http.StatusBadRequest
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
// fall back to LDAP server to see if we can find a user
|
||||
if ldapUser, ldapErr := einterfaces.GetLdapInterface().GetUser(loginId); ldapErr != nil {
|
||||
ldapErr.StatusCode = http.StatusBadRequest
|
||||
return nil, ldapErr
|
||||
} else {
|
||||
return ldapUser, nil
|
||||
}
|
||||
} else {
|
||||
Login(c, w, r, user, deviceId)
|
||||
c.LogAuditWithUserId(user.Id, "success")
|
||||
return true
|
||||
return result.Data.(*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
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 len(user.AuthData) != 0 {
|
||||
c.Err = model.NewLocAppError("LoginById", "api.user.login_by_email.sign_in.app_error",
|
||||
map[string]interface{}{"AuthService": user.AuthService}, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, mfaToken, deviceId string) *model.User {
|
||||
if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return nil
|
||||
} else {
|
||||
user := result.Data.(*model.User)
|
||||
|
||||
if len(user.AuthData) != 0 {
|
||||
c.Err = model.NewLocAppError("LoginByEmail", "api.user.login_by_email.sign_in.app_error",
|
||||
map[string]interface{}{"AuthService": user.AuthService}, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, mfaToken, deviceId string) *model.User {
|
||||
if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return nil
|
||||
} else {
|
||||
user := result.Data.(*model.User)
|
||||
|
||||
if len(user.AuthData) != 0 {
|
||||
c.Err = model.NewLocAppError("LoginByUsername", "api.user.login_by_email.sign_in.app_error",
|
||||
map[string]interface{}{"AuthService": user.AuthService}, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader) *model.User {
|
||||
buf := bytes.Buffer{}
|
||||
buf.ReadFrom(userData)
|
||||
@@ -570,80 +541,13 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st
|
||||
return nil
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
Login(c, w, r, user, "")
|
||||
doLogin(c, w, r, user, "")
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*utils.Cfg.LdapSettings.Enable {
|
||||
c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
props := model.MapFromJson(r.Body)
|
||||
|
||||
password := props["password"]
|
||||
id := props["id"]
|
||||
mfaToken := props["token"]
|
||||
|
||||
if len(password) == 0 {
|
||||
c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.blank_pwd.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
|
||||
if len(id) == 0 {
|
||||
c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.need_id.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
|
||||
ldapInterface := einterfaces.GetLdapInterface()
|
||||
if ldapInterface == nil {
|
||||
c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.not_available.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
user, err := ldapInterface.DoLogin(id, password)
|
||||
if err != nil {
|
||||
if user != nil {
|
||||
c.LogAuditWithUserId(user.Id, "attempt")
|
||||
c.LogAuditWithUserId(user.Id, "fail")
|
||||
} else {
|
||||
c.LogAudit("attempt")
|
||||
c.LogAudit("fail")
|
||||
}
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return
|
||||
}
|
||||
c.LogAuditWithUserId(user.Id, "attempt")
|
||||
|
||||
if err = checkUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "fail")
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return
|
||||
}
|
||||
|
||||
// User is authenticated at this point
|
||||
|
||||
Login(c, w, r, user, props["device_id"])
|
||||
c.LogAuditWithUserId(user.Id, "success")
|
||||
|
||||
if user != nil {
|
||||
user.Sanitize(map[string]bool{})
|
||||
} else {
|
||||
user = &model.User{}
|
||||
}
|
||||
w.Write([]byte(user.ToJson()))
|
||||
}
|
||||
|
||||
// User MUST be authenticated completely before calling Login
|
||||
func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
|
||||
func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
|
||||
|
||||
session := &model.Session{UserId: user.Id, Roles: user.Roles, DeviceId: deviceId, IsOAuth: false}
|
||||
|
||||
@@ -2371,28 +2275,20 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
loginId := props["login_id"]
|
||||
if len(loginId) == 0 {
|
||||
c.SetInvalidParam("checkMfa", "login_id")
|
||||
return
|
||||
}
|
||||
|
||||
var uchan store.StoreChannel
|
||||
if method == model.USER_AUTH_SERVICE_EMAIL {
|
||||
uchan = Srv.Store.User().GetByEmail(loginId)
|
||||
} else if method == model.USER_AUTH_SERVICE_USERNAME {
|
||||
uchan = Srv.Store.User().GetByUsername(loginId)
|
||||
} else if method == model.USER_AUTH_SERVICE_LDAP {
|
||||
uchan = Srv.Store.User().GetByAuth(loginId, model.USER_AUTH_SERVICE_LDAP)
|
||||
}
|
||||
// we don't need to worry about contacting the ldap server to get this user because
|
||||
// only users already in the system could have MFA enabled
|
||||
uchan := Srv.Store.User().GetForLogin(
|
||||
loginId,
|
||||
*utils.Cfg.EmailSettings.EnableSignInWithUsername,
|
||||
*utils.Cfg.EmailSettings.EnableSignInWithEmail,
|
||||
*utils.Cfg.LdapSettings.Enable,
|
||||
)
|
||||
|
||||
rdata := map[string]string{}
|
||||
if result := <-uchan; result.Err != nil {
|
||||
|
||||
Ссылка в новой задаче
Block a user