summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorJesús Espino <jespinog@gmail.com>2018-04-18 22:46:10 +0200
committerChristopher Speller <crspeller@gmail.com>2018-04-18 13:46:10 -0700
commit0910eae31de8ed7b409654515dbd11f5c86dbf71 (patch)
tree3d5fb47842693cd2ea1a357994c85d04902773a7 /app
parentb13a228b0451098ea32933a36fe64566e366583d (diff)
downloadchat-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.go15
-rw-r--r--app/team.go52
-rw-r--r--app/team_test.go78
-rw-r--r--app/user.go31
-rw-r--r--app/user_test.go70
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")
+ }
+ })
+}