diff options
author | Jesús Espino <jespinog@gmail.com> | 2018-04-18 22:46:10 +0200 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2018-04-18 13:46:10 -0700 |
commit | 0910eae31de8ed7b409654515dbd11f5c86dbf71 (patch) | |
tree | 3d5fb47842693cd2ea1a357994c85d04902773a7 /app | |
parent | b13a228b0451098ea32933a36fe64566e366583d (diff) | |
download | chat-0910eae31de8ed7b409654515dbd11f5c86dbf71.tar.gz chat-0910eae31de8ed7b409654515dbd11f5c86dbf71.tar.bz2 chat-0910eae31de8ed7b409654515dbd11f5c86dbf71.zip |
MM-9779: Incorporate a Token into the invitations system (#8604)
* Incorporate a Token into the invitations system
* Adding unit tests
* Fixing some api4 client tests
* Removing unnecesary hash validation
* Change the Hash concept on invitations with tokenId
* Not send invitation if it wasn't able to create the Token
* Fixing some naming problems
* Changing the hash query params received from the client side
* Removed unneded data param in the token usage
Diffstat (limited to 'app')
-rw-r--r-- | app/email.go | 15 | ||||
-rw-r--r-- | app/team.go | 52 | ||||
-rw-r--r-- | app/team_test.go | 78 | ||||
-rw-r--r-- | app/user.go | 31 | ||||
-rw-r--r-- | app/user_test.go | 70 |
5 files changed, 213 insertions, 33 deletions
diff --git a/app/email.go b/app/email.go index 7676dfe13..7a50fd82a 100644 --- a/app/email.go +++ b/app/email.go @@ -270,15 +270,22 @@ func (a *App) SendInviteEmails(team *model.Team, senderName string, invites []st bodyPage.Html["ExtraInfo"] = utils.TranslateAsHtml(utils.T, "api.templates.invite_body.extra_info", map[string]interface{}{"TeamDisplayName": team.DisplayName, "TeamURL": siteURL + "/" + team.Name}) + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": team.Id, "email": invite}), + ) + props := make(map[string]string) props["email"] = invite - props["id"] = team.Id props["display_name"] = team.DisplayName props["name"] = team.Name - props["time"] = fmt.Sprintf("%v", model.GetMillis()) data := model.MapToJson(props) - hash := utils.HashSha256(fmt.Sprintf("%v:%v", data, a.Config().EmailSettings.InviteSalt)) - bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", siteURL, url.QueryEscape(data), url.QueryEscape(hash)) + + if result := <-a.Srv.Store.Token().Save(token); result.Err != nil { + l4g.Error(utils.T("api.team.invite_members.send.error"), result.Err) + continue + } + bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(data), url.QueryEscape(token.Token)) if !a.Config().EmailSettings.SendEmailNotifications { l4g.Info(utils.T("api.team.invite_members.sending.info"), invite, bodyPage.Props["Link"]) diff --git a/app/team.go b/app/team.go index de71ed796..47e28f2ed 100644 --- a/app/team.go +++ b/app/team.go @@ -11,7 +11,6 @@ import ( "mime/multipart" "net/http" "net/url" - "strconv" "strings" l4g "github.com/alecthomas/log4go" @@ -216,19 +215,25 @@ func (a *App) AddUserToTeamByTeamId(teamId string, user *model.User) *model.AppE } } -func (a *App) AddUserToTeamByHash(userId string, hash string, data string) (*model.Team, *model.AppError) { - props := model.MapFromJson(strings.NewReader(data)) +func (a *App) AddUserToTeamByToken(userId string, tokenId string) (*model.Team, *model.AppError) { + result := <-a.Srv.Store.Token().GetByToken(tokenId) + if result.Err != nil { + return nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_invalid.app_error", nil, result.Err.Error(), http.StatusBadRequest) + } - if hash != utils.HashSha256(fmt.Sprintf("%v:%v", data, a.Config().EmailSettings.InviteSalt)) { - return nil, model.NewAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest) + token := result.Data.(*model.Token) + if token.Type != TOKEN_TYPE_TEAM_INVITATION { + return nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest) } - t, timeErr := strconv.ParseInt(props["time"], 10, 64) - if timeErr != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - return nil, model.NewAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest) + if model.GetMillis()-token.CreateAt >= TEAM_INVITATION_EXPIRY_TIME { + a.DeleteToken(token) + return nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest) } - tchan := a.Srv.Store.Team().Get(props["id"]) + tokenData := model.MapFromJson(strings.NewReader(token.Extra)) + + tchan := a.Srv.Store.Team().Get(tokenData["teamId"]) uchan := a.Srv.Store.User().Get(userId) var team *model.Team @@ -249,6 +254,10 @@ func (a *App) AddUserToTeamByHash(userId string, hash string, data string) (*mod return nil, err } + if err := a.DeleteToken(token); err != nil { + return nil, err + } + return team, nil } @@ -510,11 +519,11 @@ func (a *App) AddTeamMembers(teamId string, userIds []string, userRequestorId st return members, nil } -func (a *App) AddTeamMemberByHash(userId, hash, data string) (*model.TeamMember, *model.AppError) { +func (a *App) AddTeamMemberByToken(userId, tokenId string) (*model.TeamMember, *model.AppError) { var team *model.Team var err *model.AppError - if team, err = a.AddUserToTeamByHash(userId, hash, data); err != nil { + if team, err = a.AddUserToTeamByToken(userId, tokenId); err != nil { return nil, err } @@ -874,23 +883,28 @@ func (a *App) GetTeamStats(teamId string) (*model.TeamStats, *model.AppError) { } func (a *App) GetTeamIdFromQuery(query url.Values) (string, *model.AppError) { - hash := query.Get("h") + tokenId := query.Get("t") inviteId := query.Get("id") - if len(hash) > 0 { - data := query.Get("d") - props := model.MapFromJson(strings.NewReader(data)) + if len(tokenId) > 0 { + result := <-a.Srv.Store.Token().GetByToken(tokenId) + if result.Err != nil { + return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest) + } - if hash != utils.HashSha256(fmt.Sprintf("%v:%v", data, a.Config().EmailSettings.InviteSalt)) { + token := result.Data.(*model.Token) + if token.Type != TOKEN_TYPE_TEAM_INVITATION { return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest) } - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + if model.GetMillis()-token.CreateAt >= TEAM_INVITATION_EXPIRY_TIME { + a.DeleteToken(token) return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "", http.StatusBadRequest) } - return props["id"], nil + tokenData := model.MapFromJson(strings.NewReader(token.Extra)) + + return tokenData["teamId"], nil } else if len(inviteId) > 0 { if result := <-a.Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { // soft fail, so we still create user but don't auto-join team diff --git a/app/team_test.go b/app/team_test.go index 95f4b83d6..7ebfb8166 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -105,6 +105,84 @@ func TestAddUserToTeam(t *testing.T) { } } +func TestAddUserToTeamByToken(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + ruser, _ := th.App.CreateUser(&user) + + t.Run("invalid token", func(t *testing.T) { + if _, err := th.App.AddUserToTeamByToken(ruser.Id, "123"); err == nil { + t.Fatal("Should fail on unexisting token") + } + }) + + t.Run("invalid token type", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_VERIFY_EMAIL, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id}), + ) + <-th.App.Srv.Store.Token().Save(token) + defer th.App.DeleteToken(token) + if _, err := th.App.AddUserToTeamByToken(ruser.Id, token.Token); err == nil { + t.Fatal("Should fail on bad token type") + } + }) + + t.Run("expired token", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id}), + ) + token.CreateAt = model.GetMillis() - TEAM_INVITATION_EXPIRY_TIME - 1 + <-th.App.Srv.Store.Token().Save(token) + defer th.App.DeleteToken(token) + if _, err := th.App.AddUserToTeamByToken(ruser.Id, token.Token); err == nil { + t.Fatal("Should fail on expired token") + } + }) + + t.Run("invalid team id", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": model.NewId()}), + ) + <-th.App.Srv.Store.Token().Save(token) + defer th.App.DeleteToken(token) + if _, err := th.App.AddUserToTeamByToken(ruser.Id, token.Token); err == nil { + t.Fatal("Should fail on bad team id") + } + }) + + t.Run("invalid user id", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id}), + ) + <-th.App.Srv.Store.Token().Save(token) + defer th.App.DeleteToken(token) + if _, err := th.App.AddUserToTeamByToken(model.NewId(), token.Token); err == nil { + t.Fatal("Should fail on bad user id") + } + }) + + t.Run("valid request", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id}), + ) + <-th.App.Srv.Store.Token().Save(token) + if _, err := th.App.AddUserToTeamByToken(ruser.Id, token.Token); err != nil { + t.Log(err) + t.Fatal("Should add user to the team") + } + if result := <-th.App.Srv.Store.Token().GetByToken(token.Token); result.Err == nil { + t.Fatal("The token must be deleted after be used") + } + }) +} + func TestAddUserToTeamByTeamId(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/user.go b/app/user.go index 21165fdba..80c8b6ef2 100644 --- a/app/user.go +++ b/app/user.go @@ -34,35 +34,42 @@ import ( const ( TOKEN_TYPE_PASSWORD_RECOVERY = "password_recovery" TOKEN_TYPE_VERIFY_EMAIL = "verify_email" - PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour + TOKEN_TYPE_TEAM_INVITATION = "team_invitation" + PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour + TEAM_INVITATION_EXPIRY_TIME = 1000 * 60 * 60 * 48 // 48 hours IMAGE_PROFILE_PIXEL_DIMENSION = 128 ) -func (a *App) CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) { +func (a *App) CreateUserWithToken(user *model.User, tokenId string) (*model.User, *model.AppError) { if err := a.IsUserSignUpAllowed(); err != nil { return nil, err } - props := model.MapFromJson(strings.NewReader(data)) + result := <-a.Srv.Store.Token().GetByToken(tokenId) + if result.Err != nil { + return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, result.Err.Error(), http.StatusBadRequest) + } - if hash != utils.HashSha256(fmt.Sprintf("%v:%v", data, a.Config().EmailSettings.InviteSalt)) { - return nil, model.NewAppError("CreateUserWithHash", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusInternalServerError) + token := result.Data.(*model.Token) + if token.Type != TOKEN_TYPE_TEAM_INVITATION { + return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest) } - if t, err := strconv.ParseInt(props["time"], 10, 64); err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - return nil, model.NewAppError("CreateUserWithHash", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusInternalServerError) + if model.GetMillis()-token.CreateAt >= TEAM_INVITATION_EXPIRY_TIME { + a.DeleteToken(token) + return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest) } - teamId := props["id"] + tokenData := model.MapFromJson(strings.NewReader(token.Extra)) var team *model.Team - if result := <-a.Srv.Store.Team().Get(teamId); result.Err != nil { + if result := <-a.Srv.Store.Team().Get(tokenData["teamId"]); result.Err != nil { return nil, result.Err } else { team = result.Data.(*model.Team) } - user.Email = props["email"] + user.Email = tokenData["email"] user.EmailVerified = true var ruser *model.User @@ -77,6 +84,10 @@ func (a *App) CreateUserWithHash(user *model.User, hash string, data string) (*m a.AddDirectChannels(team.Id, ruser) + if err := a.DeleteToken(token); err != nil { + return nil, err + } + return ruser, nil } diff --git a/app/user_test.go b/app/user_test.go index 94052da61..20dafd826 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -428,3 +428,73 @@ func TestGetUsersByStatus(t *testing.T) { } }) } + +func TestCreateUserWithToken(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""} + + t.Run("invalid token", func(t *testing.T) { + if _, err := th.App.CreateUserWithToken(&user, "123"); err == nil { + t.Fatal("Should fail on unexisting token") + } + }) + + t.Run("invalid token type", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_VERIFY_EMAIL, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email}), + ) + <-th.App.Srv.Store.Token().Save(token) + defer th.App.DeleteToken(token) + if _, err := th.App.CreateUserWithToken(&user, token.Token); err == nil { + t.Fatal("Should fail on bad token type") + } + }) + + t.Run("expired token", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email}), + ) + token.CreateAt = model.GetMillis() - TEAM_INVITATION_EXPIRY_TIME - 1 + <-th.App.Srv.Store.Token().Save(token) + defer th.App.DeleteToken(token) + if _, err := th.App.CreateUserWithToken(&user, token.Token); err == nil { + t.Fatal("Should fail on expired token") + } + }) + + t.Run("invalid team id", func(t *testing.T) { + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": model.NewId(), "email": user.Email}), + ) + <-th.App.Srv.Store.Token().Save(token) + defer th.App.DeleteToken(token) + if _, err := th.App.CreateUserWithToken(&user, token.Token); err == nil { + t.Fatal("Should fail on bad team id") + } + }) + + t.Run("valid request", func(t *testing.T) { + invitationEmail := model.NewId() + "other-email@test.com" + token := model.NewToken( + TOKEN_TYPE_TEAM_INVITATION, + model.MapToJson(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail}), + ) + <-th.App.Srv.Store.Token().Save(token) + newUser, err := th.App.CreateUserWithToken(&user, token.Token) + if err != nil { + t.Log(err) + t.Fatal("Should add user to the team") + } + if newUser.Email != invitationEmail { + t.Fatal("The user email must be the invitation one") + } + if result := <-th.App.Srv.Store.Token().GetByToken(token.Token); result.Err == nil { + t.Fatal("The token must be deleted after be used") + } + }) +} |