From dfc6db737411bd4ad68a803be5182f06055a1769 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 10 Apr 2017 08:19:49 -0400 Subject: Refactor switching login type code into app layer and add v4 endpoint (#6000) * Refactor switching login type code into app layer and add v4 endpoint * Fix unit test --- api/oauth.go | 114 +--------------------------- api/user.go | 168 ++++-------------------------------------- api4/context.go | 3 +- api4/user.go | 38 ++++++++++ api4/user_test.go | 93 ++++++++++++++++++++++- app/authentication.go | 21 +++--- app/ldap.go | 78 +++++++++++++++++++- app/oauth.go | 172 ++++++++++++++++++++++++++++++++++++++++++- model/client4.go | 10 +++ model/switch_request.go | 62 ++++++++++++++++ model/switch_request_test.go | 19 +++++ 11 files changed, 494 insertions(+), 284 deletions(-) create mode 100644 model/switch_request.go create mode 100644 model/switch_request_test.go diff --git a/api/oauth.go b/api/oauth.go index 4a71500c4..059cac29b 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -4,8 +4,6 @@ package api import ( - "crypto/tls" - b64 "encoding/base64" "fmt" "io" "io/ioutil" @@ -273,7 +271,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete" - if body, teamId, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil { + if body, teamId, props, err := app.AuthorizeOAuthUser(service, code, state, uri); err != nil { c.Err = err return } else { @@ -620,7 +618,7 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { stateProps["redirect_to"] = redirectTo } - if authUrl, err := GetAuthorizationCode(c, service, stateProps, loginHint); err != nil { + if authUrl, err := app.GetAuthorizationCode(service, stateProps, loginHint); err != nil { c.Err = err return } else { @@ -680,7 +678,7 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { stateProps["team_id"] = teamId } - if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil { + if authUrl, err := app.GetAuthorizationCode(service, stateProps, ""); err != nil { c.Err = err return } else { @@ -688,112 +686,6 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } } -func GetAuthorizationCode(c *Context, service string, props map[string]string, loginHint string) (string, *model.AppError) { - - sso := utils.Cfg.GetSSOService(service) - if sso != nil && !sso.Enable { - return "", model.NewLocAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service) - } - - clientId := sso.Id - endpoint := sso.AuthEndpoint - scope := sso.Scope - - props["hash"] = model.HashPassword(clientId) - state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) - - redirectUri := c.GetSiteURLHeader() + "/signup/" + service + "/complete" - - authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) - - if len(scope) > 0 { - authUrl += "&scope=" + utils.UrlEncode(scope) - } - - if len(loginHint) > 0 { - authUrl += "&login_hint=" + utils.UrlEncode(loginHint) - } - - return authUrl, nil -} - -func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) { - sso := utils.Cfg.GetSSOService(service) - if sso == nil || !sso.Enable { - return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service) - } - - stateStr := "" - if b, err := b64.StdEncoding.DecodeString(state); err != nil { - return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, err.Error()) - } else { - stateStr = string(b) - } - - stateProps := model.MapFromJson(strings.NewReader(stateStr)) - - if !model.ComparePassword(stateProps["hash"], sso.Id) { - return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "") - } - - teamId := stateProps["team_id"] - - p := url.Values{} - p.Set("client_id", sso.Id) - p.Set("client_secret", sso.Secret) - p.Set("code", code) - p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) - p.Set("redirect_uri", redirectUri) - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, - } - client := &http.Client{Transport: tr} - req, _ := http.NewRequest("POST", sso.TokenEndpoint, strings.NewReader(p.Encode())) - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Accept", "application/json") - - var ar *model.AccessResponse - var respBody []byte - if resp, err := client.Do(req); err != nil { - return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error()) - } else { - ar = model.AccessResponseFromJson(resp.Body) - defer func() { - ioutil.ReadAll(resp.Body) - resp.Body.Close() - }() - if ar == nil { - return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, "") - } - } - - if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { - return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType+", response_body="+string(respBody)) - } - - if len(ar.AccessToken) == 0 { - return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "") - } - - p = url.Values{} - p.Set("access_token", ar.AccessToken) - req, _ = http.NewRequest("GET", sso.UserApiEndpoint, strings.NewReader("")) - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Accept", "application/json") - req.Header.Set("Authorization", "Bearer "+ar.AccessToken) - - if resp, err := client.Do(req); err != nil { - return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", - map[string]interface{}{"Service": service}, err.Error()) - } else { - return resp.Body, teamId, stateProps, nil - } - -} - func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, email string) { authData := "" ssoEmail := "" diff --git a/api/user.go b/api/user.go index 7f422b355..99f751ee0 100644 --- a/api/user.go +++ b/api/user.go @@ -914,41 +914,14 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - c.LogAudit("attempt") - - var user *model.User - var err *model.AppError - if user, err = app.GetUserByEmail(email); err != nil { - c.LogAudit("fail - couldn't get user") - c.Err = err - return - } - - if err := app.CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil { - c.LogAuditWithUserId(user.Id, "failed - bad authentication") + link, err := app.SwitchEmailToOAuth(email, password, mfaToken, service) + if err != nil { c.Err = err return } - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO - stateProps["email"] = email - - m := map[string]string{} - if service == model.USER_AUTH_SERVICE_SAML { - m["follow_link"] = c.GetSiteURLHeader() + "/login/sso/saml?action=" + model.OAUTH_ACTION_EMAIL_TO_SSO + "&email=" + email - } else { - if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil { - c.LogAuditWithUserId(user.Id, "fail - oauth issue") - c.Err = err - return - } else { - m["follow_link"] = authUrl - } - } - - c.LogAuditWithUserId(user.Id, "success") - w.Write([]byte(model.MapToJson(m))) + c.LogAudit("success for email=" + email) + w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) } func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { @@ -966,51 +939,19 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - c.LogAudit("attempt") - - var user *model.User - var err *model.AppError - if user, err = app.GetUserByEmail(email); err != nil { - c.LogAudit("fail - couldn't get user") - c.Err = err - return - } - - if user.Id != c.Session.UserId { - c.LogAudit("fail - user ids didn't match") - c.Err = model.NewLocAppError("oauthToEmail", "api.user.oauth_to_email.context.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden - return - } - - if err := app.UpdatePassword(user, password); err != nil { - c.LogAudit("fail - database issue") - c.Err = err - return - } - - go func() { - if err := app.SendSignInChangeEmail(user.Email, c.T("api.templates.signin_change_email.body.method_email"), user.Locale, utils.GetSiteURL()); err != nil { - l4g.Error(err.Error()) - } - }() - - if err := app.RevokeAllSessions(c.Session.UserId); err != nil { + link, err := app.SwitchOAuthToEmail(email, password, c.Session.UserId) + if err != nil { c.Err = err return } - c.LogAuditWithUserId(c.Session.UserId, "Revoked all sessions for user") c.RemoveSessionCookie(w, r) if c.Err != nil { return } - m := map[string]string{} - m["follow_link"] = "/login?extra=signin_change" - c.LogAudit("success") - w.Write([]byte(model.MapToJson(m))) + w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) } func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1044,55 +985,19 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") - var user *model.User - var err *model.AppError - if user, err = app.GetUserByEmail(email); err != nil { - c.LogAudit("fail - couldn't get user") - c.Err = err - return - } - - if err := app.CheckPasswordAndAllCriteria(user, emailPassword, token); err != nil { - c.LogAuditWithUserId(user.Id, "failed - bad authentication") - c.Err = err - return - } - - if err := app.RevokeAllSessions(user.Id); err != nil { + link, err := app.SwitchEmailToLdap(email, emailPassword, token, ldapId, ldapPassword) + if err != nil { c.Err = err return } - c.LogAuditWithUserId(user.Id, "Revoked all sessions for user") c.RemoveSessionCookie(w, r) if c.Err != nil { return } - ldapInterface := einterfaces.GetLdapInterface() - if ldapInterface == nil { - c.Err = model.NewLocAppError("emailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if err := ldapInterface.SwitchToLdap(user.Id, ldapId, ldapPassword); err != nil { - c.LogAuditWithUserId(user.Id, "fail - ldap switch failed") - c.Err = err - return - } - - go func() { - if err := app.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, utils.GetSiteURL()); err != nil { - l4g.Error(err.Error()) - } - }() - - m := map[string]string{} - m["follow_link"] = "/login?extra=signin_change" - c.LogAudit("success") - w.Write([]byte(model.MapToJson(m))) + w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) } func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1120,66 +1025,19 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") - var user *model.User - var err *model.AppError - if user, err = app.GetUserByEmail(email); err != nil { - c.LogAudit("fail - couldn't get user") - c.Err = err - return - } - - if user.AuthService != model.USER_AUTH_SERVICE_LDAP { - c.Err = model.NewLocAppError("ldapToEmail", "api.user.ldap_to_email.not_ldap_account.app_error", nil, "") - return - } - - ldapInterface := einterfaces.GetLdapInterface() - if ldapInterface == nil || user.AuthData == nil { - c.Err = model.NewLocAppError("ldapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if err := ldapInterface.CheckPassword(*user.AuthData, ldapPassword); err != nil { - c.LogAuditWithUserId(user.Id, "fail - ldap authentication failed") - c.Err = err - return - } - - if err := app.CheckUserMfa(user, token); err != nil { - c.LogAuditWithUserId(user.Id, "fail - mfa token failed") - c.Err = err - return - } - - if err := app.UpdatePassword(user, emailPassword); err != nil { - c.LogAudit("fail - database issue") - c.Err = err - return - } - - if err := app.RevokeAllSessions(user.Id); err != nil { + link, err := app.SwitchLdapToEmail(ldapPassword, token, email, emailPassword) + if err != nil { c.Err = err return } - c.LogAuditWithUserId(user.Id, "Revoked all sessions for user") c.RemoveSessionCookie(w, r) if c.Err != nil { return } - go func() { - if err := app.SendSignInChangeEmail(user.Email, c.T("api.templates.signin_change_email.body.method_email"), user.Locale, utils.GetSiteURL()); err != nil { - l4g.Error(err.Error()) - } - }() - - m := map[string]string{} - m["follow_link"] = "/login?extra=signin_change" - c.LogAudit("success") - w.Write([]byte(model.MapToJson(m))) + w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) } func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api4/context.go b/api4/context.go index 9c27ec9c1..cd1137508 100644 --- a/api4/context.go +++ b/api4/context.go @@ -242,8 +242,7 @@ func (c *Context) IsSystemAdmin() bool { func (c *Context) SessionRequired() { if len(c.Session.UserId) == 0 { - c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "UserRequired") - c.Err.StatusCode = http.StatusUnauthorized + c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized) return } } diff --git a/api4/user.go b/api4/user.go index b30d066ab..5880370c6 100644 --- a/api4/user.go +++ b/api4/user.go @@ -43,6 +43,7 @@ func InitUser() { BaseRoutes.User.Handle("/mfa/generate", ApiSessionRequired(generateMfaSecret)).Methods("POST") BaseRoutes.Users.Handle("/login", ApiHandler(login)).Methods("POST") + BaseRoutes.Users.Handle("/login/switch", ApiHandler(switchAccountType)).Methods("POST") BaseRoutes.Users.Handle("/logout", ApiHandler(logout)).Methods("POST") BaseRoutes.UserByUsername.Handle("", ApiSessionRequired(getUserByUsername)).Methods("GET") @@ -981,3 +982,40 @@ func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } + +func switchAccountType(c *Context, w http.ResponseWriter, r *http.Request) { + switchRequest := model.SwitchRequestFromJson(r.Body) + if switchRequest == nil { + c.SetInvalidParam("switch_request") + return + } + + link := "" + var err *model.AppError + + if switchRequest.EmailToOAuth() { + link, err = app.SwitchEmailToOAuth(switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.NewService) + } else if switchRequest.OAuthToEmail() { + c.SessionRequired() + if c.Err != nil { + return + } + + link, err = app.SwitchOAuthToEmail(switchRequest.Email, switchRequest.NewPassword, c.Session.UserId) + } else if switchRequest.EmailToLdap() { + link, err = app.SwitchEmailToLdap(switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.LdapId, switchRequest.NewPassword) + } else if switchRequest.LdapToEmail() { + link, err = app.SwitchLdapToEmail(switchRequest.Password, switchRequest.MfaCode, switchRequest.Email, switchRequest.NewPassword) + } else { + c.SetInvalidParam("switch_request") + return + } + + if err != nil { + c.Err = err + return + } + + c.LogAudit("success") + w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) +} diff --git a/api4/user_test.go b/api4/user_test.go index b3e4edc3d..f904bd460 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1297,7 +1297,7 @@ func TestUpdateUserPassword(t *testing.T) { // Should fail because account is locked out _, resp = Client.UpdateUserPassword(th.BasicUser.Id, th.BasicUser.Password, "newpwd") CheckErrorMessage(t, resp, "api.user.check_user_login_attempts.too_many.app_error") - CheckForbiddenStatus(t, resp) + CheckUnauthorizedStatus(t, resp) // System admin can update another user's password adminSetPassword := "pwdsetbyadmin" @@ -1651,3 +1651,94 @@ func TestSetProfileImage(t *testing.T) { t.Fatal(err) } } + +func TestSwitchAccount(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + enableGitLab := utils.Cfg.GitLabSettings.Enable + defer func() { + utils.Cfg.GitLabSettings.Enable = enableGitLab + }() + utils.Cfg.GitLabSettings.Enable = true + + Client.Logout() + + sr := &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_EMAIL, + NewService: model.USER_AUTH_SERVICE_GITLAB, + Email: th.BasicUser.Email, + Password: th.BasicUser.Password, + } + + link, resp := Client.SwitchAccountType(sr) + CheckNoError(t, resp) + + if link == "" { + t.Fatal("bad link") + } + + th.LoginBasic() + + fakeAuthData := "1" + if result := <-app.Srv.Store.User().UpdateAuthData(th.BasicUser.Id, model.USER_AUTH_SERVICE_GITLAB, &fakeAuthData, th.BasicUser.Email, true); result.Err != nil { + t.Fatal(result.Err) + } + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_GITLAB, + NewService: model.USER_AUTH_SERVICE_EMAIL, + Email: th.BasicUser.Email, + NewPassword: th.BasicUser.Password, + } + + link, resp = Client.SwitchAccountType(sr) + CheckNoError(t, resp) + + if link != "/login?extra=signin_change" { + t.Log(link) + t.Fatal("bad link") + } + + Client.Logout() + _, resp = Client.Login(th.BasicUser.Email, th.BasicUser.Password) + CheckNoError(t, resp) + Client.Logout() + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_GITLAB, + NewService: model.SERVICE_GOOGLE, + } + + _, resp = Client.SwitchAccountType(sr) + CheckBadRequestStatus(t, resp) + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_EMAIL, + NewService: model.USER_AUTH_SERVICE_GITLAB, + Password: th.BasicUser.Password, + } + + _, resp = Client.SwitchAccountType(sr) + CheckNotFoundStatus(t, resp) + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_EMAIL, + NewService: model.USER_AUTH_SERVICE_GITLAB, + Email: th.BasicUser.Email, + } + + _, resp = Client.SwitchAccountType(sr) + CheckUnauthorizedStatus(t, resp) + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_GITLAB, + NewService: model.USER_AUTH_SERVICE_EMAIL, + Email: th.BasicUser.Email, + NewPassword: th.BasicUser.Password, + } + + _, resp = Client.SwitchAccountType(sr) + CheckUnauthorizedStatus(t, resp) +} diff --git a/app/authentication.go b/app/authentication.go index 369458527..8ea3f5fc4 100644 --- a/app/authentication.go +++ b/app/authentication.go @@ -43,7 +43,7 @@ func checkUserPassword(user *model.User, password string) *model.AppError { return result.Err } - return model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id) + return model.NewAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized) } else { if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil { return result.Err @@ -57,8 +57,7 @@ func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaTok ldapInterface := einterfaces.GetLdapInterface() if ldapInterface == nil || ldapId == nil { - err := model.NewLocAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented + err := model.NewAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "", http.StatusNotImplemented) return nil, err } @@ -109,13 +108,13 @@ func CheckUserMfa(user *model.User, token string) *model.AppError { mfaInterface := einterfaces.GetMfaInterface() if mfaInterface == nil { - return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "") + return model.NewAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "", http.StatusNotImplemented) } if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil { return err } else if !ok { - return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "") + return model.NewAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "", http.StatusUnauthorized) } return nil @@ -123,7 +122,7 @@ func CheckUserMfa(user *model.User, token string) *model.AppError { func checkUserLoginAttempts(user *model.User) *model.AppError { if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { - return model.NewAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id, http.StatusForbidden) + return model.NewAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized) } return nil @@ -131,14 +130,14 @@ func checkUserLoginAttempts(user *model.User) *model.AppError { func checkEmailVerified(user *model.User) *model.AppError { if !user.EmailVerified && utils.Cfg.EmailSettings.RequireEmailVerification { - return model.NewLocAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id) + return model.NewAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized) } return nil } func checkUserNotDisabled(user *model.User) *model.AppError { if user.DeleteAt > 0 { - return model.NewLocAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id) + return model.NewAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized) } return nil } @@ -148,8 +147,7 @@ func authenticateUser(user *model.User, password, mfaToken string) (*model.User, if user.AuthService == model.USER_AUTH_SERVICE_LDAP { if !ldapAvailable { - err := model.NewLocAppError("login", "api.user.login_ldap.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented + err := model.NewAppError("login", "api.user.login_ldap.not_available.app_error", nil, "", http.StatusNotImplemented) return user, err } else if ldapUser, err := checkLdapUserPasswordAndAllCriteria(user.AuthData, password, mfaToken); err != nil { err.StatusCode = http.StatusUnauthorized @@ -163,8 +161,7 @@ func authenticateUser(user *model.User, password, mfaToken string) (*model.User, if authService == model.USER_AUTH_SERVICE_SAML { authService = strings.ToUpper(authService) } - err := model.NewLocAppError("login", "api.user.login.use_auth_service.app_error", map[string]interface{}{"AuthService": authService}, "") - err.StatusCode = http.StatusBadRequest + err := model.NewAppError("login", "api.user.login.use_auth_service.app_error", map[string]interface{}{"AuthService": authService}, "", http.StatusBadRequest) return user, err } else { if err := CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil { diff --git a/app/ldap.go b/app/ldap.go index fe68dfa81..94b328177 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -18,7 +18,7 @@ func SyncLdap() { if ldapI := einterfaces.GetLdapInterface(); ldapI != nil { ldapI.SyncNow() } else { - l4g.Error("%v", model.NewLocAppError("ldapSyncNow", "ent.ldap.disabled.app_error", nil, "").Error()) + l4g.Error("%v", model.NewLocAppError("SyncLdap", "ent.ldap.disabled.app_error", nil, "").Error()) } } }() @@ -31,10 +31,84 @@ func TestLdap() *model.AppError { return err } } else { - err := model.NewLocAppError("ldapTest", "ent.ldap.disabled.app_error", nil, "") + err := model.NewLocAppError("TestLdap", "ent.ldap.disabled.app_error", nil, "") err.StatusCode = http.StatusNotImplemented return err } return nil } + +func SwitchEmailToLdap(email, password, code, ldapId, ldapPassword string) (string, *model.AppError) { + user, err := GetUserByEmail(email) + if err != nil { + return "", err + } + + if err := CheckPasswordAndAllCriteria(user, password, code); err != nil { + return "", err + } + + if err := RevokeAllSessions(user.Id); err != nil { + return "", err + } + + ldapInterface := einterfaces.GetLdapInterface() + if ldapInterface == nil { + return "", model.NewAppError("SwitchEmailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusNotImplemented) + } + + if err := ldapInterface.SwitchToLdap(user.Id, ldapId, ldapPassword); err != nil { + return "", err + } + + go func() { + if err := SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, utils.GetSiteURL()); err != nil { + l4g.Error(err.Error()) + } + }() + + return "/login?extra=signin_change", nil +} + +func SwitchLdapToEmail(ldapPassword, code, email, newPassword string) (string, *model.AppError) { + user, err := GetUserByEmail(email) + if err != nil { + return "", err + } + + if user.AuthService != model.USER_AUTH_SERVICE_LDAP { + return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_ldap_account.app_error", nil, "", http.StatusBadRequest) + } + + ldapInterface := einterfaces.GetLdapInterface() + if ldapInterface == nil || user.AuthData == nil { + return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusNotImplemented) + } + + if err := ldapInterface.CheckPassword(*user.AuthData, ldapPassword); err != nil { + return "", err + } + + if err := CheckUserMfa(user, code); err != nil { + return "", err + } + + if err := UpdatePassword(user, newPassword); err != nil { + return "", err + } + + if err := RevokeAllSessions(user.Id); err != nil { + return "", err + } + + T := utils.GetUserTranslations(user.Locale) + + go func() { + if err := SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, utils.GetSiteURL()); err != nil { + l4g.Error(err.Error()) + } + }() + + return "/login?extra=signin_change", nil +} diff --git a/app/oauth.go b/app/oauth.go index 3e8b0b8d2..c357d10c8 100644 --- a/app/oauth.go +++ b/app/oauth.go @@ -4,11 +4,20 @@ package app import ( + "crypto/tls" + b64 "encoding/base64" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) func RevokeAccessToken(token string) *model.AppError { - session, _ := GetSession(token) schan := Srv.Store.Session().Remove(token) @@ -32,3 +41,164 @@ func RevokeAccessToken(token string) *model.AppError { return nil } + +func GetAuthorizationCode(service string, props map[string]string, loginHint string) (string, *model.AppError) { + sso := utils.Cfg.GetSSOService(service) + if sso != nil && !sso.Enable { + return "", model.NewLocAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service) + } + + clientId := sso.Id + endpoint := sso.AuthEndpoint + scope := sso.Scope + + props["hash"] = model.HashPassword(clientId) + state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) + + redirectUri := utils.GetSiteURL() + "/signup/" + service + "/complete" + + authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) + + if len(scope) > 0 { + authUrl += "&scope=" + utils.UrlEncode(scope) + } + + if len(loginHint) > 0 { + authUrl += "&login_hint=" + utils.UrlEncode(loginHint) + } + + return authUrl, nil +} + +func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) { + sso := utils.Cfg.GetSSOService(service) + if sso == nil || !sso.Enable { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service) + } + + stateStr := "" + if b, err := b64.StdEncoding.DecodeString(state); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, err.Error()) + } else { + stateStr = string(b) + } + + stateProps := model.MapFromJson(strings.NewReader(stateStr)) + + if !model.ComparePassword(stateProps["hash"], sso.Id) { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "") + } + + teamId := stateProps["team_id"] + + p := url.Values{} + p.Set("client_id", sso.Id) + p.Set("client_secret", sso.Secret) + p.Set("code", code) + p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + p.Set("redirect_uri", redirectUri) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} + req, _ := http.NewRequest("POST", sso.TokenEndpoint, strings.NewReader(p.Encode())) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + var ar *model.AccessResponse + var respBody []byte + if resp, err := client.Do(req); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error()) + } else { + ar = model.AccessResponseFromJson(resp.Body) + defer func() { + ioutil.ReadAll(resp.Body) + resp.Body.Close() + }() + if ar == nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, "") + } + } + + if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType+", response_body="+string(respBody)) + } + + if len(ar.AccessToken) == 0 { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "") + } + + p = url.Values{} + p.Set("access_token", ar.AccessToken) + req, _ = http.NewRequest("GET", sso.UserApiEndpoint, strings.NewReader("")) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+ar.AccessToken) + + if resp, err := client.Do(req); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", + map[string]interface{}{"Service": service}, err.Error()) + } else { + return resp.Body, teamId, stateProps, nil + } + +} + +func SwitchEmailToOAuth(email, password, code, service string) (string, *model.AppError) { + var user *model.User + var err *model.AppError + if user, err = GetUserByEmail(email); err != nil { + return "", err + } + + if err := CheckPasswordAndAllCriteria(user, password, code); err != nil { + return "", err + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO + stateProps["email"] = email + + if service == model.USER_AUTH_SERVICE_SAML { + return utils.GetSiteURL() + "/login/sso/saml?action=" + model.OAUTH_ACTION_EMAIL_TO_SSO + "&email=" + email, nil + } else { + if authUrl, err := GetAuthorizationCode(service, stateProps, ""); err != nil { + return "", err + } else { + return authUrl, nil + } + } +} + +func SwitchOAuthToEmail(email, password, requesterId string) (string, *model.AppError) { + var user *model.User + var err *model.AppError + if user, err = GetUserByEmail(email); err != nil { + return "", err + } + + if user.Id != requesterId { + return "", model.NewAppError("SwitchOAuthToEmail", "api.user.oauth_to_email.context.app_error", nil, "", http.StatusForbidden) + } + + if err := UpdatePassword(user, password); err != nil { + return "", err + } + + T := utils.GetUserTranslations(user.Locale) + + go func() { + if err := SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, utils.GetSiteURL()); err != nil { + l4g.Error(err.Error()) + } + }() + + if err := RevokeAllSessions(requesterId); err != nil { + return "", err + } + + return "/login?extra=signin_change", nil +} diff --git a/model/client4.go b/model/client4.go index 634c477e2..6d4772431 100644 --- a/model/client4.go +++ b/model/client4.go @@ -409,6 +409,16 @@ func (c *Client4) Logout() (bool, *Response) { } } +// SwitchAccountType changes a user's login type from one type to another. +func (c *Client4) SwitchAccountType(switchRequest *SwitchRequest) (string, *Response) { + if r, err := c.DoApiPost(c.GetUsersRoute()+"/login/switch", switchRequest.ToJson()); err != nil { + return "", &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return MapFromJson(r.Body)["follow_link"], BuildResponse(r) + } +} + // User Section // CreateUser creates a user in the system based on the provided user struct. diff --git a/model/switch_request.go b/model/switch_request.go new file mode 100644 index 000000000..44fbb27a9 --- /dev/null +++ b/model/switch_request.go @@ -0,0 +1,62 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type SwitchRequest struct { + CurrentService string `json:"current_service"` + NewService string `json:"new_service"` + Email string `json:"email"` + Password string `json:"current_password"` + NewPassword string `json:"new_password"` + MfaCode string `json:"mfa_code"` + LdapId string `json:"ldap_id"` +} + +func (o *SwitchRequest) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func SwitchRequestFromJson(data io.Reader) *SwitchRequest { + decoder := json.NewDecoder(data) + var o SwitchRequest + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *SwitchRequest) EmailToOAuth() bool { + return o.CurrentService == USER_AUTH_SERVICE_EMAIL && + (o.NewService == USER_AUTH_SERVICE_SAML || + o.NewService == USER_AUTH_SERVICE_GITLAB || + o.NewService == SERVICE_GOOGLE || + o.NewService == SERVICE_OFFICE365) +} + +func (o *SwitchRequest) OAuthToEmail() bool { + return (o.CurrentService == USER_AUTH_SERVICE_SAML || + o.CurrentService == USER_AUTH_SERVICE_GITLAB || + o.CurrentService == SERVICE_GOOGLE || + o.CurrentService == SERVICE_OFFICE365) && o.NewService == USER_AUTH_SERVICE_EMAIL +} + +func (o *SwitchRequest) EmailToLdap() bool { + return o.CurrentService == USER_AUTH_SERVICE_EMAIL && o.NewService == USER_AUTH_SERVICE_LDAP +} + +func (o *SwitchRequest) LdapToEmail() bool { + return o.CurrentService == USER_AUTH_SERVICE_LDAP && o.NewService == USER_AUTH_SERVICE_EMAIL +} diff --git a/model/switch_request_test.go b/model/switch_request_test.go new file mode 100644 index 000000000..b85846f78 --- /dev/null +++ b/model/switch_request_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 TestSwitchRequestJson(t *testing.T) { + o := SwitchRequest{Email: NewId(), Password: NewId()} + json := o.ToJson() + ro := SwitchRequestFromJson(strings.NewReader(json)) + + if o.Email != ro.Email { + t.Fatal("Emails do not match") + } +} -- cgit v1.2.3-1-g7c22