diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/templates/signin_change_body.html | 43 | ||||
-rw-r--r-- | api/templates/signin_change_subject.html | 1 | ||||
-rw-r--r-- | api/user.go | 340 | ||||
-rw-r--r-- | api/user_test.go | 103 |
4 files changed, 465 insertions, 22 deletions
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"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 20px 10px; text-align:left;"> + <img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You updated your sign-in method</h2> + <p>You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} to {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p> + </td> + </tr> + <tr> + {{template "email_info" . }} + </tr> + </table> + </td> + </tr> + <tr> + {{template "email_footer" . }} + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{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") + } +} |