Creating common token store and moving email invites and verification to it (#6213)

This commit is contained in:
Christopher Speller
2017-04-27 10:55:03 -04:00
committed by Joram Wilander
parent 0e007e344b
commit 9a87bb3af6
30 changed files with 461 additions and 461 deletions

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -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, "")
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -3575,6 +3575,14 @@
"id": "mattermost.working_dir",
"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"
@@ -4411,6 +4419,10 @@
"id": "model.user.is_valid.auth_data.app_error",
"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"
@@ -4591,10 +4603,6 @@
"id": "store.sql.dialect_driver.panic",
"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"

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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)
}
}

39
model/token.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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 {

108
store/sql_tokens_store.go Normal file
View File

@@ -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.")
}
}

View File

@@ -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)
//}

View File

@@ -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 {

16
utils/hash.go Normal file
View File

@@ -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))
}

View File

@@ -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);

View File

@@ -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').

View File

@@ -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));
},

View File

@@ -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});

View File

@@ -655,13 +655,12 @@ describe('Client.User', function() {
TestHelper.initBasic(done, () => {
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();
}
);