summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristopher Speller <crspeller@gmail.com>2017-04-27 10:55:03 -0400
committerJoram Wilander <jwawilander@gmail.com>2017-04-27 10:55:03 -0400
commit9a87bb3af68216b53ee8f89d6604c715c7b85b2d (patch)
tree8c06aed890f388b228f3aefb8e398309bc73c0b9
parent0e007e344bf10993529711f14c4168365c3504c3 (diff)
downloadchat-9a87bb3af68216b53ee8f89d6604c715c7b85b2d.tar.gz
chat-9a87bb3af68216b53ee8f89d6604c715c7b85b2d.tar.bz2
chat-9a87bb3af68216b53ee8f89d6604c715c7b85b2d.zip
Creating common token store and moving email invites and verification to it (#6213)
-rw-r--r--api/oauth_test.go13
-rw-r--r--api/user.go17
-rw-r--r--api/user_test.go58
-rw-r--r--api4/team_test.go6
-rw-r--r--api4/user.go55
-rw-r--r--api4/user_test.go52
-rw-r--r--app/email.go20
-rw-r--r--app/email_test.go26
-rw-r--r--app/oauth.go8
-rw-r--r--app/team.go4
-rw-r--r--app/user.go121
-rw-r--r--cmd/platform/server.go11
-rw-r--r--i18n/en.json16
-rw-r--r--model/client4.go10
-rw-r--r--model/password_recovery.go37
-rw-r--r--model/password_recovery_test.go53
-rw-r--r--model/token.go39
-rw-r--r--model/user.go4
-rw-r--r--store/sql_recovery_store.go128
-rw-r--r--store/sql_recovery_store_test.go54
-rw-r--r--store/sql_store.go28
-rw-r--r--store/sql_tokens_store.go108
-rw-r--r--store/sql_upgrade.go1
-rw-r--r--store/store.go12
-rw-r--r--utils/hash.go16
-rw-r--r--webapp/actions/user_actions.jsx9
-rw-r--r--webapp/client/client.jsx8
-rw-r--r--webapp/components/do_verify_email.jsx3
-rw-r--r--webapp/components/password_reset_form.jsx2
-rw-r--r--webapp/tests/client/client_user.test.jsx3
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();
}
);