From a6ae90ac2a74871331707751e823b4746136ff09 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Thu, 17 Dec 2015 12:44:46 -0500 Subject: Add ability to switch between SSO and email account --- api/templates/signin_change_body.html | 43 +++ api/templates/signin_change_subject.html | 1 + api/user.go | 340 +++++++++++++++++++-- api/user_test.go | 103 +++++++ config/config.json | 2 +- model/client.go | 18 ++ model/oauth.go | 7 + store/sql_user_store.go | 24 +- store/sql_user_store_test.go | 31 ++ store/store.go | 1 + web/react/components/claim/claim_account.jsx | 53 ++++ web/react/components/claim/email_to_sso.jsx | 97 ++++++ web/react/components/claim/sso_to_email.jsx | 113 +++++++ web/react/components/login.jsx | 33 +- .../user_settings/user_settings_security.jsx | 323 +++++++++++++------- web/react/pages/claim_account.jsx | 19 ++ web/react/utils/client.jsx | 34 +++ web/react/utils/constants.jsx | 2 + web/templates/claim_account.html | 16 + web/web.go | 221 +++++++------- 20 files changed, 1214 insertions(+), 267 deletions(-) create mode 100644 api/templates/signin_change_body.html create mode 100644 api/templates/signin_change_subject.html create mode 100644 web/react/components/claim/claim_account.jsx create mode 100644 web/react/components/claim/email_to_sso.jsx create mode 100644 web/react/components/claim/sso_to_email.jsx create mode 100644 web/react/pages/claim_account.jsx create mode 100644 web/templates/claim_account.html diff --git a/api/templates/signin_change_body.html b/api/templates/signin_change_body.html new file mode 100644 index 000000000..5b96df944 --- /dev/null +++ b/api/templates/signin_change_body.html @@ -0,0 +1,43 @@ +{{define "signin_change_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

You updated your sign-in method

+

You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} to {{.Props.Method}}.
If this change wasn't initiated by you, please contact your system administrator.

+
+
+
+
+ +{{end}} + + diff --git a/api/templates/signin_change_subject.html b/api/templates/signin_change_subject.html new file mode 100644 index 000000000..b1d644a28 --- /dev/null +++ b/api/templates/signin_change_subject.html @@ -0,0 +1 @@ +{{define "signin_change_subject"}}You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}} diff --git a/api/user.go b/api/user.go index 1df8fff73..36a3fd5f9 100644 --- a/api/user.go +++ b/api/user.go @@ -47,6 +47,8 @@ func InitUser(r *mux.Router) { sr.Handle("/logout", ApiUserRequired(logout)).Methods("POST") sr.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST") sr.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") + sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST") + sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST") sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") @@ -225,6 +227,70 @@ func CreateUser(team *model.Team, user *model.User) (*model.User, *model.AppErro } } +func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User { + var user *model.User + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + c.Err = model.NewAppError("CreateOAuthUser", service+" oauth not avlailable on this server", "") + return nil + } else { + user = provider.GetUserFromJson(userData) + } + + if user == nil { + c.Err = model.NewAppError("CreateOAuthUser", "Could not create user out of "+service+" user object", "") + return nil + } + + suchan := Srv.Store.User().GetByAuth(team.Id, user.AuthData, service) + euchan := Srv.Store.User().GetByEmail(team.Id, user.Email) + + if team.Email == "" { + team.Email = user.Email + if result := <-Srv.Store.Team().Update(team); result.Err != nil { + c.Err = result.Err + return nil + } + } else { + found := true + count := 0 + for found { + if found = IsUsernameTaken(user.Username, team.Id); c.Err != nil { + return nil + } else if found { + user.Username = user.Username + strconv.Itoa(count) + count += 1 + } + } + } + + if result := <-suchan; result.Err == nil { + c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email) + return nil + } + + if result := <-euchan; result.Err == nil { + c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email) + return nil + } + + user.TeamId = team.Id + user.EmailVerified = true + + ruser, err := CreateUser(team, user) + if err != nil { + c.Err = err + return nil + } + + Login(c, w, r, ruser, "") + if c.Err != nil { + return nil + } + + return ruser +} + func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { @@ -335,6 +401,11 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam } else { user := result.Data.(*model.User) + if len(user.AuthData) != 0 { + c.Err = model.NewAppError("LoginByEmail", "Please sign in using "+user.AuthService, "") + return nil + } + if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) { Login(c, w, r, user, deviceId) return user @@ -344,10 +415,36 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam return nil } +func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User { + authData := "" + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + c.Err = model.NewAppError("LoginByOAuth", service+" oauth not avlailable on this server", "") + return nil + } else { + authData = provider.GetAuthDataFromJson(userData) + } + + if len(authData) == 0 { + c.Err = model.NewAppError("LoginByOAuth", "Could not parse auth data out of "+service+" user object", "") + return nil + } + + var user *model.User + if result := <-Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil { + c.Err = result.Err + return nil + } else { + user = result.Data.(*model.User) + Login(c, w, r, user, "") + return user + } +} + func checkUserLoginAttempts(c *Context, user *model.User) bool { if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { c.LogAuditWithUserId(user.Id, "fail") - c.Err = model.NewAppError("checkUserPassword", "Your account is locked because of too many failed password attempts. Please reset your password.", "user_id="+user.Id) + c.Err = model.NewAppError("checkUserLoginAttempts", "Your account is locked because of too many failed password attempts. Please reset your password.", "user_id="+user.Id) c.Err.StatusCode = http.StatusForbidden return false } @@ -1660,21 +1757,22 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { } } -func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, teamName, service, redirectUri, loginHint string) { +func GetAuthorizationCode(c *Context, service, teamName string, props map[string]string, loginHint string) (string, *model.AppError) { sso := utils.Cfg.GetSSOService(service) if sso != nil && !sso.Enable { - c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service) - c.Err.StatusCode = http.StatusBadRequest - return + return "", model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service) } clientId := sso.Id endpoint := sso.AuthEndpoint scope := sso.Scope - stateProps := map[string]string{"team": teamName, "hash": model.HashPassword(clientId)} - state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) + props["hash"] = model.HashPassword(clientId) + props["team"] = teamName + state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) + + redirectUri := c.GetSiteURL() + "/" + service + "/complete" authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) @@ -1686,18 +1784,18 @@ func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, te authUrl += "&login_hint=" + utils.UrlEncode(loginHint) } - http.Redirect(w, r, authUrl, http.StatusFound) + return authUrl, nil } -func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, *model.AppError) { +func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, map[string]string, *model.AppError) { sso := utils.Cfg.GetSSOService(service) if sso == nil || !sso.Enable { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service) } stateStr := "" if b, err := b64.StdEncoding.DecodeString(state); err != nil { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", err.Error()) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", err.Error()) } else { stateStr = string(b) } @@ -1705,12 +1803,13 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser stateProps := model.MapFromJson(strings.NewReader(stateStr)) if !model.ComparePassword(stateProps["hash"], sso.Id) { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "") + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "") } - teamName := stateProps["team"] - if len(teamName) == 0 { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state; missing team name", "") + ok := true + teamName := "" + if teamName, ok = stateProps["team"]; !ok { + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state; missing team name", "") } tchan := Srv.Store.Team().GetByName(teamName) @@ -1730,20 +1829,20 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser var ar *model.AccessResponse if resp, err := client.Do(req); err != nil { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error()) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error()) } else { ar = model.AccessResponseFromJson(resp.Body) if ar == nil { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad response from token request", "") + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad response from token request", "") } } if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType) } if len(ar.AccessToken) == 0 { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "") + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "") } p = url.Values{} @@ -1755,12 +1854,12 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser req.Header.Set("Authorization", "Bearer "+ar.AccessToken) if resp, err := client.Do(req); err != nil { - return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error()) + return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error()) } else { if result := <-tchan; result.Err != nil { - return nil, nil, result.Err + return nil, nil, nil, result.Err } else { - return resp.Body, result.Data.(*model.Team), nil + return resp.Body, result.Data.(*model.Team), stateProps, nil } } @@ -1780,3 +1879,200 @@ func IsUsernameTaken(name string, teamId string) bool { return false } + +func switchToSSO(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + password := props["password"] + if len(password) == 0 { + c.SetInvalidParam("switchToSSO", "password") + return + } + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("switchToSSO", "team_name") + return + } + + service := props["service"] + if len(service) == 0 { + c.SetInvalidParam("switchToSSO", "service") + return + } + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("switchToSSO", "email") + return + } + + c.LogAudit("attempt") + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.LogAudit("fail - couldn't get team") + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.LogAudit("fail - couldn't get user") + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if !checkUserLoginAttempts(c, user) || !checkUserPassword(c, user, password) { + c.LogAuditWithUserId(user.Id, "fail - invalid password") + return + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO + stateProps["email"] = email + + m := map[string]string{} + if authUrl, err := GetAuthorizationCode(c, service, teamName, 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))) +} + +func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team, email string) { + authData := "" + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + c.Err = model.NewAppError("CompleteClaimWithOAuth", service+" oauth not avlailable on this server", "") + return + } else { + authData = provider.GetAuthDataFromJson(userData) + } + + if len(authData) == 0 { + c.Err = model.NewAppError("CompleteClaimWithOAuth", "Could not parse auth data out of "+service+" user object", "") + return + } + + if len(email) == 0 { + c.Err = model.NewAppError("CompleteClaimWithOAuth", "Blank email", "") + return + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + RevokeAllSession(c, user.Id) + if c.Err != nil { + return + } + + if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData); result.Err != nil { + c.Err = result.Err + return + } + + sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO") +} + +func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + password := props["password"] + if len(password) == 0 { + c.SetInvalidParam("switchToEmail", "password") + return + } + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("switchToEmail", "team_name") + return + } + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("switchToEmail", "email") + return + } + + c.LogAudit("attempt") + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.LogAudit("fail - couldn't get team") + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.LogAudit("fail - couldn't get user") + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if user.Id != c.Session.UserId { + c.LogAudit("fail - user ids didn't match") + c.Err = model.NewAppError("switchToEmail", "Update password failed because context user_id did not match provided user's id", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if result := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(password)); result.Err != nil { + c.LogAudit("fail - database issue") + c.Err = result.Err + return + } + + sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), "email and password") + + RevokeAllSession(c, c.Session.UserId) + if c.Err != nil { + return + } + + m := map[string]string{} + m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change" + + c.LogAudit("success") + w.Write([]byte(model.MapToJson(m))) +} + +func sendSignInChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) { + go func() { + + subjectPage := NewServerTemplatePage("signin_change_subject") + subjectPage.Props["SiteURL"] = siteURL + subjectPage.Props["TeamDisplayName"] = teamDisplayName + bodyPage := NewServerTemplatePage("signin_change_body") + bodyPage.Props["SiteURL"] = siteURL + bodyPage.Props["TeamDisplayName"] = teamDisplayName + bodyPage.Props["TeamURL"] = teamURL + bodyPage.Props["Method"] = method + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("Failed to send update password email successfully err=%v", err) + } + + }() +} diff --git a/api/user_test.go b/api/user_test.go index 731450321..ffa96cb66 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1085,3 +1085,106 @@ func TestStatuses(t *testing.T) { } } + +func TestSwitchToSSO(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()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + m := map[string]string{} + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - empty data") + } + + m["password"] = "pwd" + _, err := Client.SwitchToSSO(m) + if err == nil { + t.Fatal("should have failed - missing team_name, service, email") + } + + m["team_name"] = team.Name + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - missing service, email") + } + + m["service"] = "someservice" + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - missing email") + } + + m["team_name"] = "junk" + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - bad team name") + } + + m["team_name"] = team.Name + m["email"] = "junk" + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - bad email") + } + + m["email"] = ruser.Email + m["password"] = "junk" + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - bad password") + } +} + +func TestSwitchToEmail(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()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser2.Id)) + + m := map[string]string{} + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - not logged in") + } + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - empty data") + } + + m["password"] = "pwd" + _, err := Client.SwitchToSSO(m) + if err == nil { + t.Fatal("should have failed - missing team_name, service, email") + } + + m["team_name"] = team.Name + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - missing email") + } + + m["email"] = ruser.Email + m["team_name"] = "junk" + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - bad team name") + } + + m["team_name"] = team.Name + m["email"] = "junk" + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - bad email") + } + + m["email"] = ruser2.Email + if _, err := Client.SwitchToSSO(m); err == nil { + t.Fatal("should have failed - wrong user") + } +} diff --git a/config/config.json b/config/config.json index 516b96dab..5469608f8 100644 --- a/config/config.json +++ b/config/config.json @@ -100,4 +100,4 @@ "TokenEndpoint": "", "UserApiEndpoint": "" } -} \ No newline at end of file +} diff --git a/model/client.go b/model/client.go index d3f76817d..00cc1bdce 100644 --- a/model/client.go +++ b/model/client.go @@ -349,6 +349,24 @@ func (c *Client) GetSessions(id string) (*Result, *AppError) { } } +func (c *Client) SwitchToSSO(m map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/switch_to_sso", MapToJson(m)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) SwitchToEmail(m map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/switch_to_email", MapToJson(m)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + func (c *Client) Command(channelId string, command string, suggest bool) (*Result, *AppError) { m := make(map[string]string) m["command"] = command diff --git a/model/oauth.go b/model/oauth.go index 67825dd97..19f3160fc 100644 --- a/model/oauth.go +++ b/model/oauth.go @@ -10,6 +10,13 @@ import ( "unicode/utf8" ) +const ( + OAUTH_ACTION_SIGNUP = "signup" + OAUTH_ACTION_LOGIN = "login" + OAUTH_ACTION_EMAIL_TO_SSO = "email_to_sso" + OAUTH_ACTION_SSO_TO_EMAIL = "sso_to_email" +) + type OAuthApp struct { Id string `json:"id"` CreatorId string `json:"creator_id"` diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 82a2ccd05..88c4f954b 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -266,7 +266,7 @@ func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChanne updateAt := model.GetMillis() - if _, err := us.GetMaster().Exec("UPDATE Users SET Password = :Password, LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, FailedAttempts = 0 WHERE Id = :UserId AND AuthData = ''", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil { + if _, err := us.GetMaster().Exec("UPDATE Users SET Password = :Password, LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, AuthData = '', AuthService = '', FailedAttempts = 0 WHERE Id = :UserId", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil { result.Err = model.NewAppError("SqlUserStore.UpdatePassword", "We couldn't update the user password", "id="+userId+", "+err.Error()) } else { result.Data = userId @@ -298,6 +298,28 @@ func (us SqlUserStore) UpdateFailedPasswordAttempts(userId string, attempts int) return storeChannel } +func (us SqlUserStore) UpdateAuthData(userId, service, authData string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + updateAt := model.GetMillis() + + if _, err := us.GetMaster().Exec("UPDATE Users SET Password = '', LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, FailedAttempts = 0, AuthService = :AuthService, AuthData = :AuthData WHERE Id = :UserId", map[string]interface{}{"LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId, "AuthService": service, "AuthData": authData}); err != nil { + result.Err = model.NewAppError("SqlUserStore.UpdateAuthData", "We couldn't update the auth data", "id="+userId+", "+err.Error()) + } else { + result.Data = userId + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (us SqlUserStore) Get(id string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index dd08438f1..d1ee5e647 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -390,3 +390,34 @@ func TestUserStoreDelete(t *testing.T) { t.Fatal(err) } } + +func TestUserStoreUpdateAuthData(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + Must(store.User().Save(&u1)) + + service := "someservice" + authData := "1" + + if err := (<-store.User().UpdateAuthData(u1.Id, service, authData)).Err; err != nil { + t.Fatal(err) + } + + if r1 := <-store.User().GetByEmail(u1.TeamId, u1.Email); r1.Err != nil { + t.Fatal(r1.Err) + } else { + user := r1.Data.(*model.User) + if user.AuthService != service { + t.Fatal("AuthService was not updated correctly") + } + if user.AuthData != authData { + t.Fatal("AuthData was not updated correctly") + } + if user.Password != "" { + t.Fatal("Password was not cleared properly") + } + } +} diff --git a/store/store.go b/store/store.go index 682195148..8e03c8ee7 100644 --- a/store/store.go +++ b/store/store.go @@ -111,6 +111,7 @@ type UserStore interface { UpdateLastActivityAt(userId string, time int64) StoreChannel UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel UpdatePassword(userId, newPassword string) StoreChannel + UpdateAuthData(userId, service, authData string) StoreChannel Get(id string) StoreChannel GetProfiles(teamId string) StoreChannel GetByEmail(teamId string, email string) StoreChannel diff --git a/web/react/components/claim/claim_account.jsx b/web/react/components/claim/claim_account.jsx new file mode 100644 index 000000000..f38f558db --- /dev/null +++ b/web/react/components/claim/claim_account.jsx @@ -0,0 +1,53 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import EmailToSSO from './email_to_sso.jsx'; +import SSOToEmail from './sso_to_email.jsx'; + +export default class ClaimAccount extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + render() { + let content; + if (this.props.email === '') { + content =

{'No email specified.'}

; + } else if (this.props.currentType === '' && this.props.newType !== '') { + content = ( + + ); + } else { + content = ( + + ); + } + + return ( +
+ {content} +
+ ); + } +} + +ClaimAccount.defaultProps = { +}; +ClaimAccount.propTypes = { + currentType: React.PropTypes.string.isRequired, + newType: React.PropTypes.string.isRequired, + email: React.PropTypes.string.isRequired, + teamName: React.PropTypes.string.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/claim/email_to_sso.jsx b/web/react/components/claim/email_to_sso.jsx new file mode 100644 index 000000000..ac0cf876b --- /dev/null +++ b/web/react/components/claim/email_to_sso.jsx @@ -0,0 +1,97 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from '../../utils/utils.jsx'; +import * as Client from '../../utils/client.jsx'; + +export default class EmailToSSO extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + + this.state = {}; + } + submit(e) { + e.preventDefault(); + var state = {}; + + var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password) { + state.error = 'Please enter your password.'; + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var postData = {}; + postData.password = password; + postData.email = this.props.email; + postData.team_name = this.props.teamName; + postData.service = this.props.type; + + Client.switchToSSO(postData, + (data) => { + if (data.follow_link) { + window.location.href = data.follow_link; + } + }, + (error) => { + this.setState({error}); + } + ); + } + render() { + var error = null; + if (this.state.error) { + error =
; + } + + var formClass = 'form-group'; + if (error) { + formClass += ' has-error'; + } + + const uiType = Utils.toTitleCase(this.props.type) + ' SSO'; + + return ( +
+
+

{'Switch Email/Password Account to ' + uiType}

+
+

{'Upon claiming your account, you will only be able to login with ' + Utils.toTitleCase(this.props.type) + ' SSO.'}

+

{'Enter the password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}

+
+ +
+ {error} + +
+
+
+ ); + } +} + +EmailToSSO.defaultProps = { +}; +EmailToSSO.propTypes = { + type: React.PropTypes.string.isRequired, + email: React.PropTypes.string.isRequired, + teamName: React.PropTypes.string.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/claim/sso_to_email.jsx b/web/react/components/claim/sso_to_email.jsx new file mode 100644 index 000000000..0868b7f2f --- /dev/null +++ b/web/react/components/claim/sso_to_email.jsx @@ -0,0 +1,113 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from '../../utils/utils.jsx'; +import * as Client from '../../utils/client.jsx'; + +export default class SSOToEmail extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + + this.state = {}; + } + submit(e) { + e.preventDefault(); + const state = {}; + + const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password) { + state.error = 'Please enter a password.'; + this.setState(state); + return; + } + + const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim(); + if (!confirmPassword || password !== confirmPassword) { + state.error = 'Passwords do not match.'; + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var postData = {}; + postData.password = password; + postData.email = this.props.email; + postData.team_name = this.props.teamName; + + Client.switchToEmail(postData, + (data) => { + if (data.follow_link) { + window.location.href = data.follow_link; + } + }, + (error) => { + this.setState({error}); + } + ); + } + render() { + var error = null; + if (this.state.error) { + error =
; + } + + var formClass = 'form-group'; + if (error) { + formClass += ' has-error'; + } + + const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO'; + + return ( +
+
+

{'Switch ' + uiType + ' Account to Email'}

+
+

{'Upon changing your account type, you will only be able to login with your email and password.'}

+

{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}

+
+ +
+
+ +
+ {error} + +
+
+
+ ); + } +} + +SSOToEmail.defaultProps = { +}; +SSOToEmail.propTypes = { + currentType: React.PropTypes.string.isRequired, + email: React.PropTypes.string.isRequired, + teamName: React.PropTypes.string.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 9afaa8b0d..1d9b3e906 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -1,10 +1,12 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Utils from '../utils/utils.jsx'; import LoginEmail from './login_email.jsx'; import LoginLdap from './login_ldap.jsx'; +import * as Utils from '../utils/utils.jsx'; +import Constants from '../utils/constants.jsx'; + export default class Login extends React.Component { constructor(props) { super(props); @@ -40,15 +42,24 @@ export default class Login extends React.Component { ); } - const verifiedParam = Utils.getUrlParameter('verified'); - let verifiedBox = ''; - if (verifiedParam) { - verifiedBox = ( -
- - {' Email Verified'} -
- ); + const extraParam = Utils.getUrlParameter('extra'); + let extraBox = ''; + if (extraParam) { + let msg; + if (extraParam === Constants.SIGNIN_CHANGE) { + msg = ' Sign-in method changed successfully'; + } else if (extraParam === Constants.SIGNIN_VERIFIED) { + msg = ' Email Verified'; + } + + if (msg != null) { + extraBox = ( +
+ + {msg} +
+ ); + } } let emailSignup; @@ -124,7 +135,7 @@ export default class Login extends React.Component {
{'Sign in to:'}

{teamDisplayName}

{'on '}{global.window.mm_config.SiteName}

- {verifiedBox} + {extraBox} {loginMessage} {emailSignup} {ldapLogin} diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index fa2fecf07..d9c5f58a9 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -6,6 +6,9 @@ import SettingItemMax from '../setting_item_max.jsx'; import AccessHistoryModal from '../access_history_modal.jsx'; import ActivityLogModal from '../activity_log_modal.jsx'; import ToggleModalButton from '../toggle_modal_button.jsx'; + +import TeamStore from '../../stores/team_store.jsx'; + import * as Client from '../../utils/client.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import Constants from '../../utils/constants.jsx'; @@ -18,9 +21,19 @@ export default class SecurityTab extends React.Component { this.updateCurrentPassword = this.updateCurrentPassword.bind(this); this.updateNewPassword = this.updateNewPassword.bind(this); this.updateConfirmPassword = this.updateConfirmPassword.bind(this); - this.setupInitialState = this.setupInitialState.bind(this); + this.getDefaultState = this.getDefaultState.bind(this); + this.createPasswordSection = this.createPasswordSection.bind(this); + this.createSignInSection = this.createSignInSection.bind(this); - this.state = this.setupInitialState(); + this.state = this.getDefaultState(); + } + getDefaultState() { + return { + currentPassword: '', + newPassword: '', + confirmPassword: '', + authService: this.props.user.auth_service + }; } submitPassword(e) { e.preventDefault(); @@ -51,13 +64,13 @@ export default class SecurityTab extends React.Component { data.new_password = newPassword; Client.updatePassword(data, - function success() { + () => { this.props.updateSection(''); AsyncClient.getMe(); - this.setState(this.setupInitialState()); - }.bind(this), - function fail(err) { - var state = this.setupInitialState(); + this.setState(this.getDefaultState()); + }, + (err) => { + var state = this.getDefaultState(); if (err.message) { state.serverError = err.message; } else { @@ -65,7 +78,7 @@ export default class SecurityTab extends React.Component { } state.passwordError = ''; this.setState(state); - }.bind(this) + } ); } updateCurrentPassword(e) { @@ -77,86 +90,60 @@ export default class SecurityTab extends React.Component { updateConfirmPassword(e) { this.setState({confirmPassword: e.target.value}); } - setupInitialState() { - return {currentPassword: '', newPassword: '', confirmPassword: ''}; - } - render() { - var serverError; - if (this.state.serverError) { - serverError = this.state.serverError; - } - var passwordError; - if (this.state.passwordError) { - passwordError = this.state.passwordError; - } + createPasswordSection() { + let updateSectionStatus; - var updateSectionStatus; - var passwordSection; - if (this.props.activeSection === 'password') { - var inputs = []; - var submit = null; - - if (this.props.user.auth_service === '') { - inputs.push( -
- -
- -
-
- ); - inputs.push( -
- -
- -
+ if (this.props.activeSection === 'password' && this.props.user.auth_service === '') { + const inputs = []; + + inputs.push( +
+ +
+
- ); - inputs.push( -
- -
- -
+
+ ); + inputs.push( +
+ +
+
- ); - - submit = this.submitPassword; - } else { - inputs.push( -
- +
+ ); + inputs.push( +
+ +
+
- ); - } +
+ ); updateSectionStatus = function resetSection(e) { this.props.updateSection(''); @@ -164,51 +151,157 @@ export default class SecurityTab extends React.Component { e.preventDefault(); }.bind(this); - passwordSection = ( + return ( ); - } else { - var describe; - if (this.props.user.auth_service === '') { - var d = new Date(this.props.user.last_password_update); - var hour = '12'; - if (d.getHours() % 12) { - hour = String(d.getHours() % 12); - } - var min = String(d.getMinutes()); - if (d.getMinutes() < 10) { - min = '0' + d.getMinutes(); - } - var timeOfDay = ' am'; - if (d.getHours() >= 12) { - timeOfDay = ' pm'; - } + } + + var describe; + var d = new Date(this.props.user.last_password_update); + var hour = '12'; + if (d.getHours() % 12) { + hour = String(d.getHours() % 12); + } + var min = String(d.getMinutes()); + if (d.getMinutes() < 10) { + min = '0' + d.getMinutes(); + } + var timeOfDay = ' am'; + if (d.getHours() >= 12) { + timeOfDay = ' pm'; + } - describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; - } else { - describe = 'Log in done through GitLab'; + describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; + + updateSectionStatus = function updateSection() { + this.props.updateSection('password'); + }.bind(this); + + return ( + + ); + } + createSignInSection() { + let updateSectionStatus; + const user = this.props.user; + + if (this.props.activeSection === 'signin') { + const inputs = []; + const teamName = TeamStore.getCurrent().name; + + let emailOption; + if (global.window.mm_config.EnableSignUpWithEmail === 'true' && user.auth_service !== '') { + emailOption = ( + + ); } - updateSectionStatus = function updateSection() { - this.props.updateSection('password'); + let gitlabOption; + if (global.window.mm_config.EnableSignUpWithGitLab === 'true' && user.auth_service === '') { + gitlabOption = ( + + ); + } + + let googleOption; + if (global.window.mm_config.EnableSignUpWithGoogle === 'true' && user.auth_service === '') { + googleOption = ( + + ); + } + + inputs.push( +
+ {emailOption} + {gitlabOption} +
+ {googleOption} +
+ ); + + updateSectionStatus = function updateSection(e) { + this.props.updateSection(''); + this.setState({serverError: null}); + e.preventDefault(); }.bind(this); - passwordSection = ( - {'You may only have one sign-in method at a time. Switching sign-in method will send an email notifying you if the change was successful.'}; + + return ( + ); } + updateSectionStatus = function updateSection() { + this.props.updateSection('signin'); + }.bind(this); + + let describe = 'Email and Password'; + if (this.props.user.auth_service === Constants.GITLAB_SERVICE) { + describe = 'GitLab SSO'; + } + + return ( + + ); + } + render() { + const passwordSection = this.createPasswordSection(); + let signInSection; + + let numMethods = 0; + numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; + numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; + + if (global.window.mm_config.EnableSignUpWithEmail && numMethods > 0) { + signInSection = this.createSignInSection(); + } + return (
@@ -233,9 +326,11 @@ export default class SecurityTab extends React.Component {
-

Security Settings

+

{'Security Settings'}

{passwordSection} +
+ {signInSection}


, + document.getElementById('claim') + ); +} + +global.window.setup_claim_account_page = setupClaimAccountPage; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index a12e85f67..2a90da168 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -228,6 +228,40 @@ export function resetPassword(data, success, error) { track('api', 'api_users_reset_password'); } +export function switchToSSO(data, success, error) { + $.ajax({ + url: '/api/v1/users/switch_to_sso', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: function onError(xhr, status, err) { + var e = handleError('switchToSSO', xhr, status, err); + error(e); + } + }); + + track('api', 'api_users_switch_to_sso'); +} + +export function switchToEmail(data, success, error) { + $.ajax({ + url: '/api/v1/users/switch_to_email', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: function onError(xhr, status, err) { + var e = handleError('switchToEmail', xhr, status, err); + error(e); + } + }); + + track('api', 'api_users_switch_to_email'); +} + export function logout() { track('api', 'api_users_logout'); var currentTeamUrl = TeamStore.getCurrentTeamUrl(); diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 29c5ecc5d..fa5fa8e8d 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -116,6 +116,8 @@ export default { GITLAB_SERVICE: 'gitlab', GOOGLE_SERVICE: 'google', EMAIL_SERVICE: 'email', + SIGNIN_CHANGE: 'signin_change', + SIGNIN_VERIFIED: 'verified', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_FOCUS_CONTEXT_RADIUS: 10, diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html new file mode 100644 index 000000000..6c9f36fa7 --- /dev/null +++ b/web/templates/claim_account.html @@ -0,0 +1,16 @@ +{{define "claim_account"}} + + +{{template "head" . }} + +
+
+
+
+
+ + + +{{end}} diff --git a/web/web.go b/web/web.go index f1e8471b8..6e0e8df32 100644 --- a/web/web.go +++ b/web/web.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/gorilla/mux" "github.com/mattermost/platform/api" - "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" @@ -71,8 +70,7 @@ func InitWeb() { mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(loginCompleteOAuth)).Methods("GET") - mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET") + mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET") @@ -92,6 +90,7 @@ func InitWeb() { mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET") mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. @@ -565,7 +564,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { return } else { c.LogAudit("Email Verified") - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?verified=true&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) return } } @@ -687,89 +686,63 @@ func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { } } - redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SIGNUP - api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri, "") + if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } } -func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { +func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) service := params["service"] code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") - uri := c.GetSiteURL() + "/signup/" + service + "/complete" + uri := c.GetSiteURL() + "/" + service + "/complete" - if body, team, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { + if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { c.Err = err return } else { - var user *model.User - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - c.Err = model.NewAppError("signupCompleteOAuth", service+" oauth not avlailable on this server", "") - return - } else { - user = provider.GetUserFromJson(body) - } - - if user == nil { - c.Err = model.NewAppError("signupCompleteOAuth", "Could not create user out of "+service+" user object", "") - return - } - - suchan := api.Srv.Store.User().GetByAuth(team.Id, user.AuthData, service) - euchan := api.Srv.Store.User().GetByEmail(team.Id, user.Email) - - if team.Email == "" { - team.Email = user.Email - if result := <-api.Srv.Store.Team().Update(team); result.Err != nil { - c.Err = result.Err - return + action := props["action"] + switch action { + case model.OAUTH_ACTION_SIGNUP: + api.CreateOAuthUser(c, w, r, service, body, team) + if c.Err == nil { + root(c, w, r) } - } else { - found := true - count := 0 - for found { - if found = api.IsUsernameTaken(user.Username, team.Id); c.Err != nil { - return - } else if found { - user.Username = user.Username + strconv.Itoa(count) - count += 1 - } + break + case model.OAUTH_ACTION_LOGIN: + api.LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + root(c, w, r) } + break + case model.OAUTH_ACTION_EMAIL_TO_SSO: + api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) + if c.Err == nil { + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_SSO_TO_EMAIL: + api.LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) + } + break + default: + api.LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + root(c, w, r) + } + break } - - if result := <-suchan; result.Err == nil { - c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email) - return - } - - if result := <-euchan; result.Err == nil { - c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email) - return - } - - user.TeamId = team.Id - user.EmailVerified = true - - ruser, err := api.CreateUser(team, user) - if err != nil { - c.Err = err - return - } - - api.Login(c, w, r, ruser, "") - - if c.Err != nil { - return - } - - page := NewHtmlTemplatePage("home", "Home") - page.Team = team - page.User = ruser - page.Render(c, w) } } @@ -791,57 +764,14 @@ func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - redirectUri := c.GetSiteURL() + "/login/" + service + "/complete" - - api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri, loginHint) -} - -func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - - code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") - - uri := c.GetSiteURL() + "/login/" + service + "/complete" + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_LOGIN - if body, team, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { + if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { c.Err = err return } else { - authData := "" - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - c.Err = model.NewAppError("signupCompleteOAuth", service+" oauth not avlailable on this server", "") - return - } else { - authData = provider.GetAuthDataFromJson(body) - } - - if len(authData) == 0 { - c.Err = model.NewAppError("loginCompleteOAuth", "Could not parse auth data out of "+service+" user object", "") - return - } - - var user *model.User - if result := <-api.Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil { - c.Err = result.Err - return - } else { - user = result.Data.(*model.User) - api.Login(c, w, r, user, "") - - if c.Err != nil { - return - } - - page := NewHtmlTemplatePage("home", "Home") - page.Team = team - page.User = user - page.Render(c, w) - - root(c, w, r) - } + http.Redirect(w, r, authUrl, http.StatusFound) } } @@ -1172,3 +1102,58 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("ok")) } + +func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !CheckBrowserCompatability(c, r) { + return + } + + params := mux.Vars(r) + teamName := params["team"] + email := r.URL.Query().Get("email") + newType := r.URL.Query().Get("new_type") + + var team *model.Team + if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { + l4g.Error("Couldn't find team name=%v, err=%v", teamName, tResult.Err.Message) + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) + return + } else { + team = tResult.Data.(*model.Team) + } + + authType := "" + if len(email) != 0 { + if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil { + l4g.Error("Couldn't find user teamid=%v, email=%v, err=%v", team.Id, email, uResult.Err.Message) + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) + return + } else { + user := uResult.Data.(*model.User) + authType = user.AuthService + + // if user is not logged in to their SSO account, ask them to log in + if len(authType) != 0 && user.Id != c.Session.UserId { + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL + stateProps["email"] = email + + if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } + } + } + } + + page := NewHtmlTemplatePage("claim_account", "Claim Account") + page.Props["Email"] = email + page.Props["CurrentType"] = authType + page.Props["NewType"] = newType + page.Props["TeamDisplayName"] = team.DisplayName + page.Props["TeamName"] = team.Name + + page.Render(c, w) +} -- cgit v1.2.3-1-g7c22