diff options
-rw-r--r-- | api/oauth_test.go | 13 | ||||
-rw-r--r-- | api/user.go | 17 | ||||
-rw-r--r-- | api/user_test.go | 58 | ||||
-rw-r--r-- | api4/team_test.go | 6 | ||||
-rw-r--r-- | api4/user.go | 55 | ||||
-rw-r--r-- | api4/user_test.go | 52 | ||||
-rw-r--r-- | app/email.go | 20 | ||||
-rw-r--r-- | app/email_test.go | 26 | ||||
-rw-r--r-- | app/oauth.go | 8 | ||||
-rw-r--r-- | app/team.go | 4 | ||||
-rw-r--r-- | app/user.go | 121 | ||||
-rw-r--r-- | cmd/platform/server.go | 11 | ||||
-rw-r--r-- | i18n/en.json | 16 | ||||
-rw-r--r-- | model/client4.go | 10 | ||||
-rw-r--r-- | model/password_recovery.go | 37 | ||||
-rw-r--r-- | model/password_recovery_test.go | 53 | ||||
-rw-r--r-- | model/token.go | 39 | ||||
-rw-r--r-- | model/user.go | 4 | ||||
-rw-r--r-- | store/sql_recovery_store.go | 128 | ||||
-rw-r--r-- | store/sql_recovery_store_test.go | 54 | ||||
-rw-r--r-- | store/sql_store.go | 28 | ||||
-rw-r--r-- | store/sql_tokens_store.go | 108 | ||||
-rw-r--r-- | store/sql_upgrade.go | 1 | ||||
-rw-r--r-- | store/store.go | 12 | ||||
-rw-r--r-- | utils/hash.go | 16 | ||||
-rw-r--r-- | webapp/actions/user_actions.jsx | 9 | ||||
-rw-r--r-- | webapp/client/client.jsx | 8 | ||||
-rw-r--r-- | webapp/components/do_verify_email.jsx | 3 | ||||
-rw-r--r-- | webapp/components/password_reset_form.jsx | 2 | ||||
-rw-r--r-- | webapp/tests/client/client_user.test.jsx | 3 |
30 files changed, 461 insertions, 461 deletions
diff --git a/api/oauth_test.go b/api/oauth_test.go index 014facb44..3d71d8e90 100644 --- a/api/oauth_test.go +++ b/api/oauth_test.go @@ -5,16 +5,17 @@ package api import ( "encoding/base64" - "github.com/mattermost/platform/app" - "github.com/mattermost/platform/einterfaces" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" "io" "io/ioutil" "net/http" "net/url" "strings" "testing" + + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) func TestOAuthRegisterApp(t *testing.T) { @@ -735,7 +736,7 @@ func TestOAuthComplete(t *testing.T) { closeBody(r) } - stateProps["hash"] = model.HashPassword(utils.Cfg.GitLabSettings.Id) + stateProps["hash"] = utils.HashSha256(utils.Cfg.GitLabSettings.Id) state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) if r, err := HttpGet(Client.Url+"/login/gitlab/complete?code=123&state="+url.QueryEscape(state), Client.HttpClient, "", true); err == nil { t.Fatal("should have failed - no connection") @@ -771,7 +772,7 @@ func TestOAuthComplete(t *testing.T) { stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO delete(stateProps, "team_id") stateProps["redirect_to"] = utils.Cfg.GitLabSettings.AuthEndpoint - stateProps["hash"] = model.HashPassword(utils.Cfg.GitLabSettings.Id) + stateProps["hash"] = utils.HashSha256(utils.Cfg.GitLabSettings.Id) stateProps["redirect_to"] = "/oauth/authorize" state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) if r, err := HttpGet(Client.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), Client.HttpClient, "", false); err == nil { diff --git a/api/user.go b/api/user.go index 8b32dff36..eb249cb39 100644 --- a/api/user.go +++ b/api/user.go @@ -34,8 +34,8 @@ func InitUser() { BaseRoutes.Users.Handle("/logout", ApiAppHandler(logout)).Methods("POST") BaseRoutes.Users.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") BaseRoutes.Users.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") - BaseRoutes.Users.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") - BaseRoutes.Users.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") + //DEPRICATED FOR SECURITY USE APIV4 BaseRoutes.Users.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") + //DEPRICATED FOR SECURITY USE APIV4 BaseRoutes.Users.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") BaseRoutes.Users.Handle("/me", ApiUserRequired(getMe)).Methods("GET") BaseRoutes.Users.Handle("/initial_load", ApiAppHandler(getInitialLoad)).Methods("GET") @@ -767,22 +767,22 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) code := props["code"] - if len(code) != model.PASSWORD_RECOVERY_CODE_SIZE { + if len(code) != model.TOKEN_SIZE { c.SetInvalidParam("resetPassword", "code") return } newPassword := props["new_password"] - c.LogAudit("attempt - code=" + code) + c.LogAudit("attempt - token=" + code) - if err := app.ResetPasswordFromCode(code, newPassword); err != nil { - c.LogAudit("fail - code=" + code) + if err := app.ResetPasswordFromToken(code, newPassword); err != nil { + c.LogAudit("fail - token=" + code) c.Err = err return } - c.LogAudit("success - code=" + code) + c.LogAudit("success - token=" + code) rdata := map[string]string{} rdata["status"] = "ok" @@ -992,6 +992,7 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) } +/* Disabling for security reasons. Use apiv4 func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) @@ -1039,7 +1040,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { go app.SendEmailChangeVerifyEmail(user.Id, user.Email, user.Locale, utils.GetSiteURL()) } } -} +}*/ func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) { secret, err := app.GenerateMfaSecret(c.Session.UserId) diff --git a/api/user_test.go b/api/user_test.go index 5e7db1248..d9234d356 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -184,7 +184,7 @@ func TestLogin(t *testing.T) { props["display_name"] = rteam2.Data.(*model.Team).DisplayName props["time"] = fmt.Sprintf("%v", model.GetMillis()) data := model.MapToJson(props) - hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) + hash := utils.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) ruser2, err := Client.CreateUserFromSignup(&user2, data, hash) if err != nil { @@ -1316,13 +1316,6 @@ func TestResetPassword(t *testing.T) { Client.Must(Client.SendPasswordReset(user.Email)) - var recovery *model.PasswordRecovery - if result := <-app.Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { - t.Fatal(result.Err) - } else { - recovery = result.Data.(*model.PasswordRecovery) - } - //Check if the email was send to the rigth email address and the recovery key match var resultsMailbox utils.JSONMessageHeaderInbucket err := utils.RetryInbucket(5, func() error { @@ -1335,25 +1328,42 @@ func TestResetPassword(t *testing.T) { t.Log("No email was received, maybe due load on the server. Disabling this verification") } + var recoveryTokenString string if err == nil && len(resultsMailbox) > 0 { if !strings.ContainsAny(resultsMailbox[0].To[0], user.Email) { t.Fatal("Wrong To recipient") } else { if resultsEmail, err := utils.GetMessageFromMailbox(user.Email, resultsMailbox[0].ID); err == nil { - if !strings.Contains(resultsEmail.Body.Text, recovery.Code) { + loc := strings.Index(resultsEmail.Body.Text, "token=") + if loc == -1 { + t.Log(recoveryTokenString) t.Log(resultsEmail.Body.Text) - t.Log(recovery.Code) - t.Fatal("Received wrong recovery code") + t.Fatal("Code not found in email") } + loc += 6 + recoveryTokenString = resultsEmail.Body.Text[loc : loc+model.TOKEN_SIZE] + t.Log(resultsEmail.Body.Text) } } } - if _, err := Client.ResetPassword(recovery.Code, ""); err == nil { + var recoveryToken *model.Token + if result := <-app.Srv.Store.Token().GetByToken(recoveryTokenString); result.Err != nil { + t.Log(recoveryTokenString) + t.Fatal(result.Err) + } else { + recoveryToken = result.Data.(*model.Token) + } + + if recoveryToken.Token != recoveryTokenString { + t.Fatal("Did not send the correct token. DB: "+recoveryToken.Token, " Sent: "+recoveryTokenString) + } + + if _, err := Client.ResetPassword(recoveryToken.Token, ""); err == nil { t.Fatal("Should have errored - no password") } - if _, err := Client.ResetPassword(recovery.Code, "newp"); err == nil { + if _, err := Client.ResetPassword(recoveryToken.Token, "newp"); err == nil { t.Fatal("Should have errored - password too short") } @@ -1366,38 +1376,26 @@ func TestResetPassword(t *testing.T) { } code := "" - for i := 0; i < model.PASSWORD_RECOVERY_CODE_SIZE; i++ { + for i := 0; i < model.TOKEN_SIZE; i++ { code += "a" } if _, err := Client.ResetPassword(code, "newpwd1"); err == nil { t.Fatal("Should have errored - bad code") } - if _, err := Client.ResetPassword(recovery.Code, "newpwd1"); err != nil { - t.Log(recovery.Code) + if _, err := Client.ResetPassword(recoveryToken.Token, "newpwd1"); err != nil { + t.Log(recoveryToken.Token) t.Fatal(err) } - Client.Logout() - Client.Must(Client.LoginById(user.Id, "newpwd1")) - Client.SetTeamId(team.Id) - - Client.Must(Client.SendPasswordReset(user.Email)) - - if result := <-app.Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { - t.Fatal(result.Err) - } else { - recovery = result.Data.(*model.PasswordRecovery) - } - - authData := model.NewId() + /*authData := model.NewId() if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil { t.Fatal(result.Err) } if _, err := Client.ResetPassword(recovery.Code, "newpwd1"); err == nil { t.Fatal("Should have errored - sso user") - } + }*/ } func TestUserUpdateNotify(t *testing.T) { diff --git a/api4/team_test.go b/api4/team_test.go index 95b07e248..61d7dc331 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -876,7 +876,7 @@ func TestAddTeamMember(t *testing.T) { dataObject["id"] = team.Id data := model.MapToJson(dataObject) - hashed := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) + hashed := utils.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) tm, resp = Client.AddTeamMember(team.Id, "", hashed, data, "") CheckNoError(t, resp) @@ -906,7 +906,7 @@ func TestAddTeamMember(t *testing.T) { // expired data of more than 50 hours dataObject["time"] = fmt.Sprintf("%v", model.GetMillis()-1000*60*60*50) data = model.MapToJson(dataObject) - hashed = model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) + hashed = utils.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) tm, resp = Client.AddTeamMember(team.Id, "", hashed, data, "") CheckNotFoundStatus(t, resp) @@ -914,7 +914,7 @@ func TestAddTeamMember(t *testing.T) { // invalid team id dataObject["id"] = GenerateTestId() data = model.MapToJson(dataObject) - hashed = model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) + hashed = utils.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) tm, resp = Client.AddTeamMember(team.Id, "", hashed, data, "") CheckNotFoundStatus(t, resp) diff --git a/api4/user.go b/api4/user.go index 1d117ce07..18e549a64 100644 --- a/api4/user.go +++ b/api4/user.go @@ -768,23 +768,23 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) - code := props["code"] - if len(code) != model.PASSWORD_RECOVERY_CODE_SIZE { - c.SetInvalidParam("code") + token := props["token"] + if len(token) != model.TOKEN_SIZE { + c.SetInvalidParam("token") return } newPassword := props["new_password"] - c.LogAudit("attempt - code=" + code) + c.LogAudit("attempt - token=" + token) - if err := app.ResetPasswordFromCode(code, newPassword); err != nil { - c.LogAudit("fail - code=" + code) + if err := app.ResetPasswordFromToken(token, newPassword); err != nil { + c.LogAudit("fail - token=" + token) c.Err = err return } - c.LogAudit("success - code=" + code) + c.LogAudit("success - token=" + token) ReturnStatusOK(w) } @@ -985,32 +985,21 @@ func getUserAudits(c *Context, w http.ResponseWriter, r *http.Request) { func verifyUserEmail(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) - userId := props["user_id"] - if len(userId) != 26 { - c.SetInvalidParam("user_id") + token := props["token"] + if len(token) != model.TOKEN_SIZE { + c.SetInvalidParam("token") return } - hashedId := props["hash_id"] - if len(hashedId) == 0 { - c.SetInvalidParam("hash_id") + if err := app.VerifyEmailFromToken(token); err != nil { + c.Err = model.NewLocAppError("verifyUserEmail", "api.user.verify_email.bad_link.app_error", nil, err.Error()) + c.Err.StatusCode = http.StatusBadRequest + return + } else { + c.LogAudit("Email Verified") + ReturnStatusOK(w) return } - - hashed := model.HashPassword(hashedId) - if model.ComparePassword(hashed, userId+utils.Cfg.EmailSettings.InviteSalt) { - if c.Err = app.VerifyUserEmail(userId); c.Err != nil { - return - } else { - c.LogAudit("Email Verified") - ReturnStatusOK(w) - return - } - } - - c.Err = model.NewLocAppError("verifyUserEmail", "api.user.verify_email.bad_link.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest - return } func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1029,10 +1018,12 @@ func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - if _, err := app.GetStatus(user.Id); err != nil { - go app.SendVerifyEmail(user.Id, user.Email, user.Locale, utils.GetSiteURL()) - } else { - go app.SendEmailChangeVerifyEmail(user.Id, user.Email, user.Locale, utils.GetSiteURL()) + app.SendEmailVerification(user) + if err != nil { + // Don't want to leak whether the email is valid or not + l4g.Error("Unable to create email verification token: " + err.Error()) + ReturnStatusOK(w) + return } ReturnStatusOK(w) diff --git a/api4/user_test.go b/api4/user_test.go index 20def9a17..ae0dd79e5 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1413,13 +1413,6 @@ func TestResetPassword(t *testing.T) { t.Fatal("should have succeeded") } - var recovery *model.PasswordRecovery - if result := <-app.Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil { - t.Fatal(result.Err) - } else { - recovery = result.Data.(*model.PasswordRecovery) - } - // Check if the email was send to the right email address and the recovery key match var resultsMailbox utils.JSONMessageHeaderInbucket err := utils.RetryInbucket(5, func() error { @@ -1431,24 +1424,36 @@ func TestResetPassword(t *testing.T) { t.Log(err) t.Log("No email was received, maybe due load on the server. Disabling this verification") } + + var recoveryTokenString string if err == nil && len(resultsMailbox) > 0 { if !strings.ContainsAny(resultsMailbox[0].To[0], user.Email) { t.Fatal("Wrong To recipient") } else { if resultsEmail, err := utils.GetMessageFromMailbox(user.Email, resultsMailbox[0].ID); err == nil { - if !strings.Contains(resultsEmail.Body.Text, recovery.Code) { + loc := strings.Index(resultsEmail.Body.Text, "token=") + if loc == -1 { t.Log(resultsEmail.Body.Text) - t.Log(recovery.Code) - t.Fatal("Received wrong recovery code") + t.Fatal("Code not found in email") } + loc += 6 + recoveryTokenString = resultsEmail.Body.Text[loc : loc+model.TOKEN_SIZE] } } } - _, resp = Client.ResetPassword(recovery.Code, "") + var recoveryToken *model.Token + if result := <-app.Srv.Store.Token().GetByToken(recoveryTokenString); result.Err != nil { + t.Log(recoveryTokenString) + t.Fatal(result.Err) + } else { + recoveryToken = result.Data.(*model.Token) + } + + _, resp = Client.ResetPassword(recoveryToken.Token, "") CheckBadRequestStatus(t, resp) - _, resp = Client.ResetPassword(recovery.Code, "newp") + _, resp = Client.ResetPassword(recoveryToken.Token, "newp") CheckBadRequestStatus(t, resp) _, resp = Client.ResetPassword("", "newpwd") @@ -1458,14 +1463,14 @@ func TestResetPassword(t *testing.T) { CheckBadRequestStatus(t, resp) code := "" - for i := 0; i < model.PASSWORD_RECOVERY_CODE_SIZE; i++ { + for i := 0; i < model.TOKEN_SIZE; i++ { code += "a" } _, resp = Client.ResetPassword(code, "newpwd") CheckBadRequestStatus(t, resp) - success, resp = Client.ResetPassword(recovery.Code, "newpwd") + success, resp = Client.ResetPassword(recoveryToken.Token, "newpwd") CheckNoError(t, resp) if !success { t.Fatal("should have succeeded") @@ -1474,16 +1479,16 @@ func TestResetPassword(t *testing.T) { Client.Login(user.Email, "newpwd") Client.Logout() - _, resp = Client.ResetPassword(recovery.Code, "newpwd") + _, resp = Client.ResetPassword(recoveryToken.Token, "newpwd") CheckBadRequestStatus(t, resp) - authData := model.NewId() + /*authData := model.NewId() if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil { t.Fatal(result.Err) } _, resp = Client.SendPasswordResetEmail(user.Email) - CheckBadRequestStatus(t, resp) + CheckBadRequestStatus(t, resp)*/ } func TestGetSessions(t *testing.T) { @@ -1646,15 +1651,18 @@ func TestVerifyUserEmail(t *testing.T) { ruser, resp := Client.CreateUser(&user) - hashId := ruser.Id + utils.Cfg.EmailSettings.InviteSalt - _, resp = Client.VerifyUserEmail(ruser.Id, hashId) + token, err := app.CreateVerifyEmailToken(ruser.Id) + if err != nil { + t.Fatal("Unable to create email verify token") + } + + _, resp = Client.VerifyUserEmail(token.Token) CheckNoError(t, resp) - hashId = ruser.Id + GenerateTestId() - _, resp = Client.VerifyUserEmail(ruser.Id, hashId) + _, resp = Client.VerifyUserEmail(GenerateTestId()) CheckBadRequestStatus(t, resp) - _, resp = Client.VerifyUserEmail(ruser.Id, "") + _, resp = Client.VerifyUserEmail("") CheckBadRequestStatus(t, resp) } diff --git a/app/email.go b/app/email.go index 235d949be..cd3cb3b4f 100644 --- a/app/email.go +++ b/app/email.go @@ -33,10 +33,10 @@ func SendChangeUsernameEmail(oldUsername, newUsername, email, locale, siteURL st return nil } -func SendEmailChangeVerifyEmail(userId, newUserEmail, locale, siteURL string) *model.AppError { +func SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) *model.AppError { T := utils.GetUserTranslations(locale) - link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(newUserEmail)) + link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token, url.QueryEscape(newUserEmail)) subject := T("api.templates.email_change_verify_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], @@ -77,10 +77,10 @@ func SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) *model.App return nil } -func SendVerifyEmail(userId, userEmail, locale, siteURL string) *model.AppError { +func SendVerifyEmail(userEmail, locale, siteURL, token string) *model.AppError { T := utils.GetUserTranslations(locale) - link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(userEmail)) + link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token, url.QueryEscape(userEmail)) url, _ := url.Parse(siteURL) @@ -144,7 +144,11 @@ func SendWelcomeEmail(userId string, email string, verified bool, locale, siteUR } if !verified { - link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(email)) + token, err := CreateVerifyEmailToken(userId) + if err != nil { + return err + } + link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token.Token, url.QueryEscape(email)) bodyPage.Props["VerifyUrl"] = link } @@ -175,11 +179,11 @@ func SendPasswordChangeEmail(email, method, locale, siteURL string) *model.AppEr return nil } -func SendPasswordResetEmail(email string, recovery *model.PasswordRecovery, locale, siteURL string) (bool, *model.AppError) { +func SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, *model.AppError) { T := utils.GetUserTranslations(locale) - link := fmt.Sprintf("%s/reset_password_complete?code=%s", siteURL, url.QueryEscape(recovery.Code)) + link := fmt.Sprintf("%s/reset_password_complete?token=%s", siteURL, url.QueryEscape(token.Token)) subject := T("api.templates.reset_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) @@ -252,7 +256,7 @@ func SendInviteEmails(team *model.Team, senderName string, invites []string, sit props["name"] = team.Name props["time"] = fmt.Sprintf("%v", model.GetMillis()) data := model.MapToJson(props) - hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) + hash := utils.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", siteURL, url.QueryEscape(data), url.QueryEscape(hash)) if !utils.Cfg.EmailSettings.SendEmailNotifications { diff --git a/app/email_test.go b/app/email_test.go index 3f57c54f9..b1aa9976c 100644 --- a/app/email_test.go +++ b/app/email_test.go @@ -62,17 +62,17 @@ func TestSendChangeUsernameEmail(t *testing.T) { func TestSendEmailChangeVerifyEmail(t *testing.T) { Setup() - var userId string = "5349853498543jdfvndf9834" var newUserEmail string = "newtest@example.com" var locale string = "en" var siteURL string = "" var expectedPartialMessage string = "You updated your email" var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Verify new email address" + var token string = "TEST_TOKEN" //Delete all the messages before check the sample email utils.DeleteMailBox(newUserEmail) - if err := SendEmailChangeVerifyEmail(userId, newUserEmail, locale, siteURL); err != nil { + if err := SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token); err != nil { t.Log(err) t.Fatal("Should send change username email") } else { @@ -160,17 +160,17 @@ func TestSendEmailChangeEmail(t *testing.T) { func TestSendVerifyEmail(t *testing.T) { Setup() - var userId string = "5349853498543jdfvndf9834" var userEmail string = "test@example.com" var locale string = "en" var siteURL string = "" var expectedPartialMessage string = "Please verify your email address by clicking below" var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Email Verification" + var token string = "TEST_TOKEN" //Delete all the messages before check the sample email utils.DeleteMailBox(userEmail) - if err := SendVerifyEmail(userId, userEmail, locale, siteURL); err != nil { + if err := SendVerifyEmail(userEmail, locale, siteURL, token); err != nil { t.Log(err) t.Fatal("Should send change username email") } else { @@ -582,14 +582,22 @@ func TestSendPasswordReset(t *testing.T) { t.Log(resultsEmail.Body.Text) t.Fatal("Wrong Body message") } - var recoveryKey *model.PasswordRecovery - if result := <-Srv.Store.PasswordRecovery().Get(th.BasicUser.Id); result.Err != nil { + loc := strings.Index(resultsEmail.Body.Text, "token=") + if loc == -1 { + t.Log(resultsEmail.Body.Text) + t.Fatal("Code not found in email") + } + loc += 6 + recoveryTokenString := resultsEmail.Body.Text[loc : loc+model.TOKEN_SIZE] + var recoveryToken *model.Token + if result := <-Srv.Store.Token().GetByToken(recoveryTokenString); result.Err != nil { + t.Log(recoveryTokenString) t.Fatal(result.Err) } else { - recoveryKey = result.Data.(*model.PasswordRecovery) - if !strings.Contains(resultsEmail.Body.Text, recoveryKey.Code) { + recoveryToken = result.Data.(*model.Token) + if !strings.Contains(resultsEmail.Body.Text, recoveryToken.Token) { t.Log(resultsEmail.Body.Text) - t.Log(recoveryKey.Code) + t.Log(recoveryToken.Token) t.Fatal("Received wrong recovery code") } } diff --git a/app/oauth.go b/app/oauth.go index 5bbe744d9..03e3c507b 100644 --- a/app/oauth.go +++ b/app/oauth.go @@ -109,7 +109,7 @@ func AllowOAuthAppAccessToUser(userId string, authRequest *model.AuthorizeReques } authData := &model.AuthData{UserId: userId, ClientId: authRequest.ClientId, CreateAt: model.GetMillis(), RedirectUri: authRequest.RedirectUri, State: authRequest.State, Scope: authRequest.Scope} - authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", authRequest.ClientId, authRequest.RedirectUri, authData.CreateAt, userId)) + authData.Code = utils.HashSha256(fmt.Sprintf("%v:%v:%v:%v", authRequest.ClientId, authRequest.RedirectUri, authData.CreateAt, userId)) // this saves the OAuth2 app as authorized authorizedApp := model.Preference{ @@ -167,7 +167,7 @@ func GetOAuthAccessToken(clientId, grantType, redirectUri, code, secret, refresh return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "", http.StatusBadRequest) } - if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { + if code != utils.HashSha256(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusBadRequest) } @@ -530,7 +530,7 @@ func GetAuthorizationCode(service string, props map[string]string, loginHint str endpoint := sso.AuthEndpoint scope := sso.Scope - props["hash"] = model.HashPassword(clientId) + props["hash"] = utils.HashSha256(clientId) state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) redirectUri := utils.GetSiteURL() + "/signup/" + service + "/complete" @@ -563,7 +563,7 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser stateProps := model.MapFromJson(strings.NewReader(stateStr)) - if !model.ComparePassword(stateProps["hash"], sso.Id) { + if stateProps["hash"] != utils.HashSha256(sso.Id) { return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "") } diff --git a/app/team.go b/app/team.go index d4e6d6308..47f4f5c15 100644 --- a/app/team.go +++ b/app/team.go @@ -198,7 +198,7 @@ func AddUserToTeamByTeamId(teamId string, user *model.User) *model.AppError { func AddUserToTeamByHash(userId string, hash string, data string) (*model.Team, *model.AppError) { props := model.MapFromJson(strings.NewReader(data)) - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + if hash != utils.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { return nil, model.NewLocAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_invalid.app_error", nil, "") } @@ -757,7 +757,7 @@ func GetTeamIdFromQuery(query url.Values) (string, *model.AppError) { data := query.Get("d") props := model.MapFromJson(strings.NewReader(data)) - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + if hash != utils.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest) } diff --git a/app/user.go b/app/user.go index 86e7cf0b0..3d33fb317 100644 --- a/app/user.go +++ b/app/user.go @@ -30,6 +30,13 @@ import ( "github.com/mattermost/platform/utils" ) +const ( + TOKEN_TYPE_PASSWORD_RECOVERY = "password_recovery" + TOKEN_TYPE_VERIFY_EMAIL = "verify_email" + PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour + VERIFY_EMAIL_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour +) + func CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) { if err := IsUserSignUpAllowed(); err != nil { return nil, err @@ -37,7 +44,7 @@ func CreateUserWithHash(user *model.User, hash string, data string) (*model.User props := model.MapFromJson(strings.NewReader(data)) - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + if hash != utils.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_invalid.app_error", nil, "") } @@ -978,11 +985,9 @@ func UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.A }() if utils.Cfg.EmailSettings.RequireEmailVerification { - go func() { - if err := SendEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, rusers[0].Locale, utils.GetSiteURL()); err != nil { - l4g.Error(err.Error()) - } - }() + if err := SendEmailVerification(rusers[0]); err != nil { + l4g.Error(err.Error()) + } } } @@ -1084,19 +1089,19 @@ func UpdatePasswordSendEmail(user *model.User, newPassword, method string) *mode return nil } -func ResetPasswordFromCode(code, newPassword string) *model.AppError { - var recovery *model.PasswordRecovery +func ResetPasswordFromToken(userSuppliedTokenString, newPassword string) *model.AppError { + var token *model.Token var err *model.AppError - if recovery, err = GetPasswordRecovery(code); err != nil { + if token, err = GetPasswordRecoveryToken(userSuppliedTokenString); err != nil { return err } else { - if model.GetMillis()-recovery.CreateAt >= model.PASSWORD_RECOVER_EXPIRY_TIME { + if model.GetMillis()-token.CreateAt >= PASSWORD_RECOVER_EXPIRY_TIME { return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest) } } var user *model.User - if user, err = GetUser(recovery.UserId); err != nil { + if user, err = GetUser(token.Extra); err != nil { return err } @@ -1110,7 +1115,7 @@ func ResetPasswordFromCode(code, newPassword string) *model.AppError { return err } - if err := DeletePasswordRecoveryForUser(recovery.UserId); err != nil { + if err := DeleteToken(token); err != nil { l4g.Error(err.Error()) } @@ -1128,39 +1133,42 @@ func SendPasswordReset(email string, siteURL string) (bool, *model.AppError) { return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest) } - var recovery *model.PasswordRecovery - if recovery, err = CreatePasswordRecovery(user.Id); err != nil { + var token *model.Token + if token, err = CreatePasswordRecoveryToken(user.Id); err != nil { return false, err } - if _, err := SendPasswordResetEmail(email, recovery, user.Locale, siteURL); err != nil { + if _, err := SendPasswordResetEmail(email, token, user.Locale, siteURL); err != nil { return false, model.NewLocAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message) } return true, nil } -func CreatePasswordRecovery(userId string) (*model.PasswordRecovery, *model.AppError) { - recovery := &model.PasswordRecovery{} - recovery.UserId = userId +func CreatePasswordRecoveryToken(userId string) (*model.Token, *model.AppError) { + token := model.NewToken(TOKEN_TYPE_PASSWORD_RECOVERY, userId) - if result := <-Srv.Store.PasswordRecovery().SaveOrUpdate(recovery); result.Err != nil { + if result := <-Srv.Store.Token().Save(token); result.Err != nil { return nil, result.Err } - return recovery, nil + return token, nil } -func GetPasswordRecovery(code string) (*model.PasswordRecovery, *model.AppError) { - if result := <-Srv.Store.PasswordRecovery().GetByCode(code); result.Err != nil { - return nil, model.NewAppError("GetPasswordRecovery", "api.user.reset_password.invalid_link.app_error", nil, result.Err.Error(), http.StatusBadRequest) +func GetPasswordRecoveryToken(token string) (*model.Token, *model.AppError) { + if result := <-Srv.Store.Token().GetByToken(token); result.Err != nil { + return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.invalid_link.app_error", nil, result.Err.Error(), http.StatusBadRequest) } else { - return result.Data.(*model.PasswordRecovery), nil + token := result.Data.(*model.Token) + if token.Type != TOKEN_TYPE_PASSWORD_RECOVERY { + return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.broken_token.app_error", nil, "", http.StatusBadRequest) + } + return token, nil } } -func DeletePasswordRecoveryForUser(userId string) *model.AppError { - if result := <-Srv.Store.PasswordRecovery().Delete(userId); result.Err != nil { +func DeleteToken(token *model.Token) *model.AppError { + if result := <-Srv.Store.Token().Delete(token.Token); result.Err != nil { return result.Err } @@ -1250,10 +1258,6 @@ func PermanentDeleteUser(user *model.User) *model.AppError { return result.Err } - if result := <-Srv.Store.PasswordRecovery().Delete(user.Id); result.Err != nil { - return result.Err - } - l4g.Warn(utils.T("api.user.permanent_delete_user.deleted.warn"), user.Email, user.Id) return nil @@ -1272,6 +1276,63 @@ func PermanentDeleteAllUsers() *model.AppError { return nil } +func SendEmailVerification(user *model.User) *model.AppError { + token, err := CreateVerifyEmailToken(user.Id) + if err != nil { + return err + } + + if _, err := GetStatus(user.Id); err != nil { + go SendVerifyEmail(user.Email, user.Locale, utils.GetSiteURL(), token.Token) + } else { + go SendEmailChangeVerifyEmail(user.Email, user.Locale, utils.GetSiteURL(), token.Token) + } + + return nil +} + +func VerifyEmailFromToken(userSuppliedTokenString string) *model.AppError { + var token *model.Token + var err *model.AppError + if token, err = GetVerifyEmailToken(userSuppliedTokenString); err != nil { + return err + } else { + if model.GetMillis()-token.CreateAt >= PASSWORD_RECOVER_EXPIRY_TIME { + return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest) + } + if err := VerifyUserEmail(token.Extra); err != nil { + return err + } + if err := DeleteToken(token); err != nil { + l4g.Error(err.Error()) + } + } + + return nil +} + +func CreateVerifyEmailToken(userId string) (*model.Token, *model.AppError) { + token := model.NewToken(TOKEN_TYPE_VERIFY_EMAIL, userId) + + if result := <-Srv.Store.Token().Save(token); result.Err != nil { + return nil, result.Err + } + + return token, nil +} + +func GetVerifyEmailToken(token string) (*model.Token, *model.AppError) { + if result := <-Srv.Store.Token().GetByToken(token); result.Err != nil { + return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.bad_link.app_error", nil, result.Err.Error(), http.StatusBadRequest) + } else { + token := result.Data.(*model.Token) + if token.Type != TOKEN_TYPE_VERIFY_EMAIL { + return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.broken_token.app_error", nil, "", http.StatusBadRequest) + } + return token, nil + } +} + func VerifyUserEmail(userId string) *model.AppError { if err := (<-Srv.Store.User().VerifyEmail(userId)).Err; err != nil { return err diff --git a/cmd/platform/server.go b/cmd/platform/server.go index 0d971afb6..01c8b646a 100644 --- a/cmd/platform/server.go +++ b/cmd/platform/server.go @@ -100,6 +100,8 @@ func runServer(configFileLocation string) { go runSecurityJob() go runDiagnosticsJob() + go runTokenCleanupJob() + if complianceI := einterfaces.GetComplianceInterface(); complianceI != nil { complianceI.StartComplianceDailyJob() } @@ -139,6 +141,11 @@ func runDiagnosticsJob() { model.CreateRecurringTask("Diagnostics", doDiagnostics, time.Hour*24) } +func runTokenCleanupJob() { + doTokenCleanup() + model.CreateRecurringTask("Token Cleanup", doTokenCleanup, time.Hour*1) +} + func resetStatuses() { if result := <-app.Srv.Store.Status().ResetAll(); result.Err != nil { l4g.Error(utils.T("mattermost.reset_status.error"), result.Err.Error()) @@ -169,3 +176,7 @@ func doDiagnostics() { app.SendDailyDiagnostics() } } + +func doTokenCleanup() { + app.Srv.Store.Token().Cleanup() +} diff --git a/i18n/en.json b/i18n/en.json index 83d0b83e2..06fb3528c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3576,6 +3576,14 @@ "translation": "Current working directory is %v" }, { + "id": "model.token.is_valid.size", + "translation": "Invalid token." + }, + { + "id": "model.token.is_valid.expiry", + "translation": "Invalid token expiry" + }, + { "id": "model.access.is_valid.access_token.app_error", "translation": "Invalid access token" }, @@ -4412,6 +4420,10 @@ "translation": "Invalid auth data" }, { + "id": "model.user.is_valid.password_limit.app_error", + "translation": "Unable to set a password over 72 charactors due to the limitations of bcrypt." + }, + { "id": "model.user.is_valid.auth_data_pwd.app_error", "translation": "Invalid user, password and auth data cannot both be set" }, @@ -4592,10 +4604,6 @@ "translation": "Failed to create dialect specific driver %v" }, { - "id": "store.sql.drop_column.critical", - "translation": "Failed to drop column %v" - }, - { "id": "store.sql.incorrect_mac", "translation": "Incorrect MAC for the given ciphertext" }, diff --git a/model/client4.go b/model/client4.go index a33e62846..3a6507f82 100644 --- a/model/client4.go +++ b/model/client4.go @@ -751,8 +751,8 @@ func (c *Client4) SendPasswordResetEmail(email string) (bool, *Response) { } // ResetPassword uses a recovery code to update reset a user's password. -func (c *Client4) ResetPassword(code, newPassword string) (bool, *Response) { - requestBody := map[string]string{"code": code, "new_password": newPassword} +func (c *Client4) ResetPassword(token, newPassword string) (bool, *Response) { + requestBody := map[string]string{"token": token, "new_password": newPassword} if r, err := c.DoApiPost(c.GetUsersRoute()+"/password/reset", MapToJson(requestBody)); err != nil { return false, &Response{StatusCode: r.StatusCode, Error: err} } else { @@ -821,9 +821,9 @@ func (c *Client4) GetUserAudits(userId string, page int, perPage int, etag strin } } -// VerifyUserEmail will verify a user's email using user id and hash strings. -func (c *Client4) VerifyUserEmail(userId, hashId string) (bool, *Response) { - requestBody := map[string]string{"user_id": userId, "hash_id": hashId} +// VerifyUserEmail will verify a user's email using the supplied token. +func (c *Client4) VerifyUserEmail(token string) (bool, *Response) { + requestBody := map[string]string{"token": token} if r, err := c.DoApiPost(c.GetUsersRoute()+"/email/verify", MapToJson(requestBody)); err != nil { return false, &Response{StatusCode: r.StatusCode, Error: err} } else { diff --git a/model/password_recovery.go b/model/password_recovery.go deleted file mode 100644 index 8af046642..000000000 --- a/model/password_recovery.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -const ( - PASSWORD_RECOVERY_CODE_SIZE = 128 - PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour -) - -type PasswordRecovery struct { - UserId string - Code string - CreateAt int64 -} - -func (p *PasswordRecovery) IsValid() *AppError { - - if len(p.UserId) != 26 { - return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.user_id.app_error", nil, "") - } - - if len(p.Code) != PASSWORD_RECOVERY_CODE_SIZE { - return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.code.app_error", nil, "") - } - - if p.CreateAt == 0 { - return NewLocAppError("User.IsValid", "model.password_recovery.is_valid.create_at.app_error", nil, "") - } - - return nil -} - -func (p *PasswordRecovery) PreSave() { - p.Code = NewRandomString(PASSWORD_RECOVERY_CODE_SIZE) - p.CreateAt = GetMillis() -} diff --git a/model/password_recovery_test.go b/model/password_recovery_test.go deleted file mode 100644 index d64f430fc..000000000 --- a/model/password_recovery_test.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -import ( - "strings" - "testing" -) - -func TestPasswordRecoveryIsValid(t *testing.T) { - // Valid example. - p := PasswordRecovery{ - UserId: NewId(), - Code: strings.Repeat("a", 128), - CreateAt: GetMillis(), - } - - if err := p.IsValid(); err != nil { - t.Fatal(err) - } - - // Various invalid ones. - p.UserId = "abc" - if err := p.IsValid(); err == nil { - t.Fatal("Should have failed validation") - } - - p.UserId = NewId() - p.Code = "abc" - if err := p.IsValid(); err == nil { - t.Fatal("Should have failed validation") - } - - p.Code = strings.Repeat("a", 128) - p.CreateAt = 0 - if err := p.IsValid(); err == nil { - t.Fatal("Should have failed validation") - } -} - -func TestPasswordRecoveryPreSave(t *testing.T) { - p := PasswordRecovery{ - UserId: NewId(), - } - - // Check it's valid after running PreSave - p.PreSave() - - if err := p.IsValid(); err != nil { - t.Fatal(err) - } -} diff --git a/model/token.go b/model/token.go new file mode 100644 index 000000000..54cbd210e --- /dev/null +++ b/model/token.go @@ -0,0 +1,39 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import "net/http" + +const ( + TOKEN_SIZE = 128 + MAX_TOKEN_EXIPRY_TIME = 1000 * 60 * 60 * 24 // 24 hour +) + +type Token struct { + Token string + CreateAt int64 + Type string + Extra string +} + +func NewToken(tokentype, extra string) *Token { + return &Token{ + Token: NewRandomString(TOKEN_SIZE), + CreateAt: GetMillis(), + Type: tokentype, + Extra: extra, + } +} + +func (t *Token) IsValid() *AppError { + if len(t.Token) != TOKEN_SIZE { + return NewAppError("Token.IsValid", "model.token.is_valid.size", nil, "", http.StatusInternalServerError) + } + + if t.CreateAt == 0 { + return NewAppError("Token.IsValid", "model.token.is_valid.expiry", nil, "", http.StatusInternalServerError) + } + + return nil +} diff --git a/model/user.go b/model/user.go index 1c390a121..f983139f9 100644 --- a/model/user.go +++ b/model/user.go @@ -130,6 +130,10 @@ func (u *User) IsValid() *AppError { return InvalidUserError("auth_data_pwd", u.Id) } + if len(u.Password) > 72 { + return InvalidUserError("password_limit", u.Id) + } + return nil } diff --git a/store/sql_recovery_store.go b/store/sql_recovery_store.go deleted file mode 100644 index 6c073a548..000000000 --- a/store/sql_recovery_store.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package store - -import ( - "database/sql" - "net/http" - - "github.com/mattermost/platform/model" -) - -type SqlPasswordRecoveryStore struct { - *SqlStore -} - -func NewSqlPasswordRecoveryStore(sqlStore *SqlStore) PasswordRecoveryStore { - s := &SqlPasswordRecoveryStore{sqlStore} - - for _, db := range sqlStore.GetAllConns() { - table := db.AddTableWithName(model.PasswordRecovery{}, "PasswordRecovery").SetKeys(false, "UserId") - table.ColMap("UserId").SetMaxSize(26) - table.ColMap("Code").SetMaxSize(128) - } - - return s -} - -func (s SqlPasswordRecoveryStore) CreateIndexesIfNotExists() { - s.CreateIndexIfNotExists("idx_password_recovery_code", "PasswordRecovery", "Code") -} - -func (s SqlPasswordRecoveryStore) SaveOrUpdate(recovery *model.PasswordRecovery) StoreChannel { - - storeChannel := make(StoreChannel, 1) - - go func() { - result := StoreResult{} - - recovery.PreSave() - if result.Err = recovery.IsValid(); result.Err != nil { - storeChannel <- result - close(storeChannel) - return - } - - if err := s.GetReplica().SelectOne(&model.PasswordRecovery{}, "SELECT * FROM PasswordRecovery WHERE UserId = :UserId", map[string]interface{}{"UserId": recovery.UserId}); err == nil { - if _, err := s.GetMaster().Update(recovery); err != nil { - result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.SaveOrUpdate", "store.sql_recover.update.app_error", nil, "") - } - } else { - if err := s.GetMaster().Insert(recovery); err != nil { - result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.SaveOrUpdate", "store.sql_recover.save.app_error", nil, "") - } - } - - storeChannel <- result - close(storeChannel) - }() - - return storeChannel -} - -func (s SqlPasswordRecoveryStore) Delete(userId string) StoreChannel { - - storeChannel := make(StoreChannel, 1) - - go func() { - result := StoreResult{} - - if _, err := s.GetMaster().Exec("DELETE FROM PasswordRecovery WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}); err != nil { - result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.Delete", "store.sql_recover.delete.app_error", nil, "") - } - - storeChannel <- result - close(storeChannel) - }() - - return storeChannel -} - -func (s SqlPasswordRecoveryStore) Get(userId string) StoreChannel { - - storeChannel := make(StoreChannel, 1) - - go func() { - result := StoreResult{} - - recovery := model.PasswordRecovery{} - - if err := s.GetReplica().SelectOne(&recovery, "SELECT * FROM PasswordRecovery WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}); err != nil { - result.Err = model.NewLocAppError("SqlPasswordRecoveryStore.Get", "store.sql_recover.get.app_error", nil, "") - } - - result.Data = &recovery - - storeChannel <- result - close(storeChannel) - }() - - return storeChannel -} - -func (s SqlPasswordRecoveryStore) GetByCode(code string) StoreChannel { - - storeChannel := make(StoreChannel, 1) - - go func() { - result := StoreResult{} - - recovery := model.PasswordRecovery{} - - if err := s.GetReplica().SelectOne(&recovery, "SELECT * FROM PasswordRecovery WHERE Code = :Code", map[string]interface{}{"Code": code}); err != nil { - if err == sql.ErrNoRows { - result.Err = model.NewAppError("SqlPasswordRecoveryStore.GetByCode", "store.sql_recover.get_by_code.app_error", nil, "", http.StatusBadRequest) - } else { - result.Err = model.NewAppError("SqlPasswordRecoveryStore.GetByCode", "store.sql_recover.get_by_code.app_error", nil, "", http.StatusInternalServerError) - } - } - - result.Data = &recovery - - storeChannel <- result - close(storeChannel) - }() - - return storeChannel -} diff --git a/store/sql_recovery_store_test.go b/store/sql_recovery_store_test.go deleted file mode 100644 index 39f5c068b..000000000 --- a/store/sql_recovery_store_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package store - -import ( - "github.com/mattermost/platform/model" - "testing" -) - -func TestSqlPasswordRecoveryGet(t *testing.T) { - Setup() - - recovery := &model.PasswordRecovery{UserId: "12345678901234567890123456"} - Must(store.PasswordRecovery().SaveOrUpdate(recovery)) - - result := <-store.PasswordRecovery().Get(recovery.UserId) - rrecovery := result.Data.(*model.PasswordRecovery) - if rrecovery.Code != recovery.Code { - t.Fatal("codes didn't match") - } - - result2 := <-store.PasswordRecovery().GetByCode(recovery.Code) - rrecovery2 := result2.Data.(*model.PasswordRecovery) - if rrecovery2.Code != recovery.Code { - t.Fatal("codes didn't match") - } -} - -func TestSqlPasswordRecoverySaveOrUpdate(t *testing.T) { - Setup() - - recovery := &model.PasswordRecovery{UserId: "12345678901234567890123456"} - - if err := (<-store.PasswordRecovery().SaveOrUpdate(recovery)).Err; err != nil { - t.Fatal(err) - } - - // not duplicate, testing update - if err := (<-store.PasswordRecovery().SaveOrUpdate(recovery)).Err; err != nil { - t.Fatal(err) - } -} - -func TestSqlPasswordRecoveryDelete(t *testing.T) { - Setup() - - recovery := &model.PasswordRecovery{UserId: "12345678901234567890123456"} - Must(store.PasswordRecovery().SaveOrUpdate(recovery)) - - if err := (<-store.PasswordRecovery().Delete(recovery.UserId)).Err; err != nil { - t.Fatal(err) - } -} diff --git a/store/sql_store.go b/store/sql_store.go index 466042ca2..0d921d07d 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -62,6 +62,7 @@ const ( EXIT_REMOVE_INDEX_POSTGRES = 121 EXIT_REMOVE_INDEX_MYSQL = 122 EXIT_REMOVE_INDEX_MISSING = 123 + EXIT_REMOVE_TABLE = 134 ) type SqlStore struct { @@ -80,7 +81,7 @@ type SqlStore struct { command CommandStore preference PreferenceStore license LicenseStore - recovery PasswordRecoveryStore + token TokenStore emoji EmojiStore status StatusStore fileInfo FileInfoStore @@ -131,7 +132,7 @@ func NewSqlStore() Store { sqlStore.command = NewSqlCommandStore(sqlStore) sqlStore.preference = NewSqlPreferenceStore(sqlStore) sqlStore.license = NewSqlLicenseStore(sqlStore) - sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore) + sqlStore.token = NewSqlTokenStore(sqlStore) sqlStore.emoji = NewSqlEmojiStore(sqlStore) sqlStore.status = NewSqlStatusStore(sqlStore) sqlStore.fileInfo = NewSqlFileInfoStore(sqlStore) @@ -159,7 +160,7 @@ func NewSqlStore() Store { sqlStore.command.(*SqlCommandStore).CreateIndexesIfNotExists() sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists() sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists() - sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists() + sqlStore.token.(*SqlTokenStore).CreateIndexesIfNotExists() sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists() sqlStore.status.(*SqlStatusStore).CreateIndexesIfNotExists() sqlStore.fileInfo.(*SqlFileInfoStore).CreateIndexesIfNotExists() @@ -388,7 +389,7 @@ func (ss *SqlStore) RemoveColumnIfExists(tableName string, columnName string) bo _, err := ss.GetMaster().Exec("ALTER TABLE " + tableName + " DROP COLUMN " + columnName) if err != nil { - l4g.Critical(utils.T("store.sql.drop_column.critical"), err) + l4g.Critical("Failed to drop column %v", err) time.Sleep(time.Second) os.Exit(EXIT_REMOVE_COLUMN) } @@ -396,6 +397,21 @@ func (ss *SqlStore) RemoveColumnIfExists(tableName string, columnName string) bo return true } +func (ss *SqlStore) RemoveTableIfExists(tableName string) bool { + if !ss.DoesTableExist(tableName) { + return false + } + + _, err := ss.GetMaster().Exec("DROP TABLE " + tableName) + if err != nil { + l4g.Critical("Failed to drop table %v", err) + time.Sleep(time.Second) + os.Exit(EXIT_REMOVE_TABLE) + } + + return true +} + func (ss *SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool { if !ss.DoesColumnExist(tableName, oldColumnName) { return false @@ -667,8 +683,8 @@ func (ss *SqlStore) License() LicenseStore { return ss.license } -func (ss *SqlStore) PasswordRecovery() PasswordRecoveryStore { - return ss.recovery +func (ss *SqlStore) Token() TokenStore { + return ss.token } func (ss *SqlStore) Emoji() EmojiStore { diff --git a/store/sql_tokens_store.go b/store/sql_tokens_store.go new file mode 100644 index 000000000..3ee03b549 --- /dev/null +++ b/store/sql_tokens_store.go @@ -0,0 +1,108 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "database/sql" + "net/http" + + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/platform/model" +) + +type SqlTokenStore struct { + *SqlStore +} + +func NewSqlTokenStore(sqlStore *SqlStore) TokenStore { + s := &SqlTokenStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Token{}, "Tokens").SetKeys(false, "Token") + table.ColMap("Type").SetMaxSize(64) + table.ColMap("Extra").SetMaxSize(128) + } + + return s +} + +func (s SqlTokenStore) CreateIndexesIfNotExists() { +} + +func (s SqlTokenStore) Save(token *model.Token) StoreChannel { + + storeChannel := make(StoreChannel, 1) + + go func() { + result := StoreResult{} + + if result.Err = token.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(token); err != nil { + result.Err = model.NewLocAppError("SqlTokenStore.Save", "store.sql_recover.save.app_error", nil, "") + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTokenStore) Delete(token string) StoreChannel { + + storeChannel := make(StoreChannel, 1) + + go func() { + result := StoreResult{} + + if _, err := s.GetMaster().Exec("DELETE FROM Tokens WHERE Token = :Token", map[string]interface{}{"Token": token}); err != nil { + result.Err = model.NewLocAppError("SqlTokenStore.Delete", "store.sql_recover.delete.app_error", nil, "") + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTokenStore) GetByToken(tokenString string) StoreChannel { + + storeChannel := make(StoreChannel, 1) + + go func() { + result := StoreResult{} + + token := model.Token{} + + if err := s.GetReplica().SelectOne(&token, "SELECT * FROM Tokens WHERE Token = :Token", map[string]interface{}{"Token": tokenString}); err != nil { + if err == sql.ErrNoRows { + result.Err = model.NewAppError("SqlTokenStore.GetByToken", "store.sql_recover.get_by_code.app_error", nil, err.Error(), http.StatusBadRequest) + } else { + result.Err = model.NewAppError("SqlTokenStore.GetByToken", "store.sql_recover.get_by_code.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + result.Data = &token + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTokenStore) Cleanup() { + l4g.Debug("Cleaning up token store.") + deltime := model.GetMillis() - model.MAX_TOKEN_EXIPRY_TIME + if _, err := s.GetMaster().Exec("DELETE FROM Tokens WHERE CreateAt < :DelTime", map[string]interface{}{"DelTime": deltime}); err != nil { + l4g.Error("Unable to cleanup token store.") + } +} diff --git a/store/sql_upgrade.go b/store/sql_upgrade.go index b25e7e73d..f708f15c5 100644 --- a/store/sql_upgrade.go +++ b/store/sql_upgrade.go @@ -258,6 +258,7 @@ func UpgradeDatabaseToVersion39(sqlStore *SqlStore) { // TODO: Uncomment following condition when version 3.9.0 is released //if shouldPerformUpgrade(sqlStore, VERSION_3_8_0, VERSION_3_9_0) { sqlStore.CreateColumnIfNotExists("OAuthAccessData", "Scope", "varchar(128)", "varchar(128)", model.DEFAULT_SCOPE) + sqlStore.RemoveTableIfExists("PasswordRecovery") // saveSchemaVersion(sqlStore, VERSION_3_9_0) //} diff --git a/store/store.go b/store/store.go index 18f7374dc..570695bfe 100644 --- a/store/store.go +++ b/store/store.go @@ -42,7 +42,7 @@ type Store interface { Command() CommandStore Preference() PreferenceStore License() LicenseStore - PasswordRecovery() PasswordRecoveryStore + Token() TokenStore Emoji() EmojiStore Status() StatusStore FileInfo() FileInfoStore @@ -322,11 +322,11 @@ type LicenseStore interface { Get(id string) StoreChannel } -type PasswordRecoveryStore interface { - SaveOrUpdate(recovery *model.PasswordRecovery) StoreChannel - Delete(userId string) StoreChannel - Get(userId string) StoreChannel - GetByCode(code string) StoreChannel +type TokenStore interface { + Save(recovery *model.Token) StoreChannel + Delete(token string) StoreChannel + GetByToken(token string) StoreChannel + Cleanup() } type EmojiStore interface { diff --git a/utils/hash.go b/utils/hash.go new file mode 100644 index 000000000..cc82fd94b --- /dev/null +++ b/utils/hash.go @@ -0,0 +1,16 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "crypto/sha256" + "fmt" +) + +func HashSha256(text string) string { + hash := sha256.New() + hash.Write([]byte(text)) + + return fmt.Sprintf("%x", hash.Sum(nil)) +} diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx index c4e0f4fc6..aa43dafda 100644 --- a/webapp/actions/user_actions.jsx +++ b/webapp/actions/user_actions.jsx @@ -655,10 +655,9 @@ export function updatePassword(userId, currentPassword, newPassword, success, er ); } -export function verifyEmail(uid, hid, success, error) { +export function verifyEmail(token, success, error) { Client.verifyEmail( - uid, - hid, + token, (data) => { if (success) { success(data); @@ -672,9 +671,9 @@ export function verifyEmail(uid, hid, success, error) { ); } -export function resetPassword(code, password, success, error) { +export function resetPassword(token, password, success, error) { Client.resetPassword( - code, + token, password, () => { browserHistory.push('/login?extra=' + ActionTypes.PASSWORD_CHANGE); diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 697687a5f..2b149e240 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -1319,19 +1319,19 @@ export default class Client { this.trackEvent('api', 'api_channels_set_active', {channel_id: id}); } - verifyEmail(uid, hid, success, error) { + verifyEmail(token, success, error) { request. - post(`${this.getUsersRoute()}/verify_email`). + post(`${this.url}/api/v4/users/email/verify`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - send({uid, hid}). + send({token}). end(this.handleResponse.bind(this, 'verifyEmail', success, error)); } resendVerification(email, success, error) { request. - post(`${this.getUsersRoute()}/resend_verification`). + post(`${this.url}/api/v4/users/email/verify/send`). set(this.defaultHeaders). type('application/json'). accept('application/json'). diff --git a/webapp/components/do_verify_email.jsx b/webapp/components/do_verify_email.jsx index 9065e6bc4..eeb22e4a8 100644 --- a/webapp/components/do_verify_email.jsx +++ b/webapp/components/do_verify_email.jsx @@ -21,8 +21,7 @@ export default class DoVerifyEmail extends React.Component { } componentWillMount() { verifyEmail( - this.props.location.query.uid, - this.props.location.query.hid, + this.props.location.query.token, () => { browserHistory.push('/login?extra=verified&email=' + encodeURIComponent(this.props.location.query.email)); }, diff --git a/webapp/components/password_reset_form.jsx b/webapp/components/password_reset_form.jsx index 0d67eb786..546dce1b3 100644 --- a/webapp/components/password_reset_form.jsx +++ b/webapp/components/password_reset_form.jsx @@ -43,7 +43,7 @@ class PasswordResetForm extends React.Component { }); resetPassword( - this.props.location.query.code, + this.props.location.query.token, password, () => { this.setState({error: null}); diff --git a/webapp/tests/client/client_user.test.jsx b/webapp/tests/client/client_user.test.jsx index d10ad6ef0..480d7df10 100644 --- a/webapp/tests/client/client_user.test.jsx +++ b/webapp/tests/client/client_user.test.jsx @@ -656,12 +656,11 @@ describe('Client.User', function() { TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error TestHelper.basicClient().verifyEmail( 'junk', - 'junk', function() { done.fail(new Error('should be invalid')); }, function(err) { - expect(err.id).toBe('api.context.invalid_param.app_error'); + expect(err.id).toBe('api.context.invalid_body_param.app_error'); done(); } ); |