From bd946c8f9e50f93b4ac30d88259ea508deea163f Mon Sep 17 00:00:00 2001 From: dimashasbi Date: Sat, 9 Apr 2022 22:22:32 +0700 Subject: [PATCH 1/4] create redirect url and validation --- server/api.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ server/plugin.go | 1 + 2 files changed, 49 insertions(+) diff --git a/server/api.go b/server/api.go index 7fd432c9..a5c3b1fe 100644 --- a/server/api.go +++ b/server/api.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "sync" "github.com/mattermost/mattermost-server/v5/mlog" @@ -36,6 +37,13 @@ type StartMeetingFromAction struct { } `json:"context"` } +type CallbackValidation struct { + UserID string + ChannelID string + room string + Jwt string +} + func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { switch path := r.URL.Path; path { case "/api/v1/meetings/enrich": @@ -46,6 +54,8 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req p.handleConfig(w, r) case "/jitsi_meet_external_api.js": p.handleExternalAPIjs(w, r) + case "/auth-callback": + p.handleCallback(w, r) default: http.NotFound(w, r) } @@ -197,6 +207,12 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) { return } + callbackValidation := CallbackValidation{ + UserID: userID, + ChannelID: channelID, + } + p.callback = callbackValidation + userConfig, err := p.getUserConfig(userID) if err != nil { mlog.Error("Error getting user config", mlog.Err(err)) @@ -303,3 +319,35 @@ func (p *Plugin) handleEnrichMeetingJwt(w http.ResponseWriter, r *http.Request) mlog.Warn("Unable to write response body", mlog.String("handler", "handleEnrichMeetingJwt"), mlog.Err(err)) } } + +func (p *Plugin) handleCallback(w http.ResponseWriter, r *http.Request) { + if err := p.getConfiguration().IsValid(); err != nil { + mlog.Error("Invalid plugin configuration", mlog.Err(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + room := r.Header.Get("room") + + user, err := p.API.GetUser(p.callback.UserID) + if err != nil { + http.Error(w, err.Error(), err.StatusCode) + } + + jwtToken, err2 := p.updateJwtUserInfo(p.callback.Jwt, user) + if err2 != nil { + mlog.Error("Error updating JWT context", mlog.Err(err2)) + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } + + jitsiURL := strings.TrimSpace(p.getConfiguration().GetJitsiURL()) + jitsiURL = strings.TrimRight(jitsiURL, "/") + redirectURL := jitsiURL + "/" + room + "?jwt=" + + // if valid + redirectURL = redirectURL + jwtToken + + http.Redirect(w, r, redirectURL, http.StatusSeeOther) + +} diff --git a/server/plugin.go b/server/plugin.go index a0ece18a..c1fb7343 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -35,6 +35,7 @@ type Plugin struct { telemetryClient telemetry.Client tracker telemetry.Tracker + callback CallbackValidation // configurationLock synchronizes access to the configuration. configurationLock sync.RWMutex From 71896fc56124e1253179313b80bbc4d1333c3eab Mon Sep 17 00:00:00 2001 From: dimashasbi Date: Mon, 11 Apr 2022 17:06:05 +0700 Subject: [PATCH 2/4] add jwt --- server/api.go | 85 ++++++++++++++++++++++++++++++++++++---------- server/api_test.go | 11 ++++++ server/plugin.go | 7 ++++ 3 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 server/api_test.go diff --git a/server/api.go b/server/api.go index a5c3b1fe..7decb9da 100644 --- a/server/api.go +++ b/server/api.go @@ -3,13 +3,17 @@ package main import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "net/http" + "net/url" "os" "path/filepath" "strings" "sync" + "time" + "github.com/cristalhq/jwt/v2" "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin" @@ -41,7 +45,6 @@ type CallbackValidation struct { UserID string ChannelID string room string - Jwt string } func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { @@ -207,12 +210,6 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) { return } - callbackValidation := CallbackValidation{ - UserID: userID, - ChannelID: channelID, - } - p.callback = callbackValidation - userConfig, err := p.getUserConfig(userID) if err != nil { mlog.Error("Error getting user config", mlog.Err(err)) @@ -334,20 +331,74 @@ func (p *Plugin) handleCallback(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), err.StatusCode) } - jwtToken, err2 := p.updateJwtUserInfo(p.callback.Jwt, user) - if err2 != nil { - mlog.Error("Error updating JWT context", mlog.Err(err2)) - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - jitsiURL := strings.TrimSpace(p.getConfiguration().GetJitsiURL()) jitsiURL = strings.TrimRight(jitsiURL, "/") redirectURL := jitsiURL + "/" + room + "?jwt=" - // if valid - redirectURL = redirectURL + jwtToken + checkCallback, err2 := checkValidationCallback(user, room) + if err2 != nil { + jwtToken, err1 := p.createJwtToken(user, "invalidroom") + if err1 != nil { + mlog.Error("Error Create JWT context", mlog.Err(err1)) + http.Error(w, "Internal error", http.StatusInternalServerError) + } + redirectURL = redirectURL + jwtToken + http.Redirect(w, r, redirectURL, http.StatusSeeOther) + } + if checkCallback { + jwtToken, err1 := p.createJwtToken(user, room) + if err1 != nil { + mlog.Error("Error Create JWT context", mlog.Err(err1)) + http.Error(w, "Internal error", http.StatusInternalServerError) + } + redirectURL = redirectURL + jwtToken + http.Redirect(w, r, redirectURL, http.StatusSeeOther) + } else { + redirectURL = *p.API.GetConfig().ServiceSettings.SiteURL + http.Redirect(w, r, redirectURL, http.StatusSeeOther) + } +} + +func (p *Plugin) createJwtToken(user *model.User, room string) (string, error) { + // Error check is done in configuration.IsValid() + jURL, _ := url.Parse(p.getConfiguration().GetJitsiURL()) + + var meetingLinkValidUntil = time.Time{} + meetingLinkValidUntil = time.Now().Add(time.Duration(p.getConfiguration().JitsiLinkValidTime) * time.Minute) + + claims := Claims{} + claims.Issuer = p.getConfiguration().JitsiAppID + claims.Audience = []string{p.getConfiguration().JitsiAppID} + claims.ExpiresAt = jwt.NewNumericDate(meetingLinkValidUntil) + claims.Subject = jURL.Hostname() + claims.Room = room + + sanitizedUser := user.DeepCopy() + config := p.API.GetConfig() + if config.PrivacySettings.ShowFullName == nil || !*config.PrivacySettings.ShowFullName { + sanitizedUser.FirstName = "" + sanitizedUser.LastName = "" + } + if config.PrivacySettings.ShowEmailAddress == nil || !*config.PrivacySettings.ShowEmailAddress { + sanitizedUser.Email = "" + } + + newContext := Context{ + User: User{ + Avatar: fmt.Sprintf("%s/api/v4/users/%s/image?_=%d", *config.ServiceSettings.SiteURL, sanitizedUser.Id, sanitizedUser.LastPictureUpdate), + Name: sanitizedUser.GetDisplayName(model.SHOW_NICKNAME_FULLNAME), + Email: sanitizedUser.Email, + ID: sanitizedUser.Id, + }, + Group: claims.Context.Group, + } + claims.Context = newContext + + return signClaims(p.getConfiguration().JitsiAppSecret, &claims) + +} - http.Redirect(w, r, redirectURL, http.StatusSeeOther) +func checkValidationCallback(user *model.User, room string) (bool, error) { + return true, nil } diff --git a/server/api_test.go b/server/api_test.go new file mode 100644 index 00000000..c9e74b80 --- /dev/null +++ b/server/api_test.go @@ -0,0 +1,11 @@ +package main + +import "testing" + +func TestCallback(t *testing.T) { + // Case Success + + // Case Fail Authenticate + + // Case +} diff --git a/server/plugin.go b/server/plugin.go index c1fb7343..f60db510 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -356,6 +356,13 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI }) + "\n\n" + meetingUntil, } + callbackValidation := CallbackValidation{ + UserID: user.Id, + ChannelID: channel.Id, + room: meetingID, + } + p.callback = callbackValidation + post := &model.Post{ UserId: user.Id, ChannelId: channel.Id, From e0882d087317a4a7b88af9918640a8ea2d39a181 Mon Sep 17 00:00:00 2001 From: dimashasbi Date: Tue, 12 Apr 2022 13:28:10 +0700 Subject: [PATCH 3/4] add validation --- server/api.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/api.go b/server/api.go index 7decb9da..b0a6c78e 100644 --- a/server/api.go +++ b/server/api.go @@ -335,7 +335,7 @@ func (p *Plugin) handleCallback(w http.ResponseWriter, r *http.Request) { jitsiURL = strings.TrimRight(jitsiURL, "/") redirectURL := jitsiURL + "/" + room + "?jwt=" - checkCallback, err2 := checkValidationCallback(user, room) + checkCallback, err2 := p.checkValidationCallback(user, room) if err2 != nil { jwtToken, err1 := p.createJwtToken(user, "invalidroom") if err1 != nil { @@ -395,10 +395,13 @@ func (p *Plugin) createJwtToken(user *model.User, room string) (string, error) { claims.Context = newContext return signClaims(p.getConfiguration().JitsiAppSecret, &claims) - } -func checkValidationCallback(user *model.User, room string) (bool, error) { - +func (p *Plugin) checkValidationCallback(user *model.User, room string) (bool, error) { + if p.callback.room != room { + return false, nil + } else if p.callback.UserID != user.Id { + return false, nil + } return true, nil } From 5dd4be6dc9fcea7c457bd46dd01348dcc788ae1d Mon Sep 17 00:00:00 2001 From: dimashasbi Date: Thu, 14 Apr 2022 14:03:41 +0700 Subject: [PATCH 4/4] create better function and add unit test --- server/api.go | 58 ++++++++++++++----------------------- server/api_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 42 deletions(-) diff --git a/server/api.go b/server/api.go index b0a6c78e..2e2eb0eb 100644 --- a/server/api.go +++ b/server/api.go @@ -323,46 +323,39 @@ func (p *Plugin) handleCallback(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - room := r.Header.Get("room") + redirectURL := p.handleRedirectURL(room) + http.Redirect(w, r, redirectURL, http.StatusSeeOther) +} - user, err := p.API.GetUser(p.callback.UserID) - if err != nil { - http.Error(w, err.Error(), err.StatusCode) - } - +func (p *Plugin) handleRedirectURL(room string) string { jitsiURL := strings.TrimSpace(p.getConfiguration().GetJitsiURL()) jitsiURL = strings.TrimRight(jitsiURL, "/") redirectURL := jitsiURL + "/" + room + "?jwt=" - checkCallback, err2 := p.checkValidationCallback(user, room) - if err2 != nil { - jwtToken, err1 := p.createJwtToken(user, "invalidroom") - if err1 != nil { - mlog.Error("Error Create JWT context", mlog.Err(err1)) - http.Error(w, "Internal error", http.StatusInternalServerError) - } - redirectURL = redirectURL + jwtToken - http.Redirect(w, r, redirectURL, http.StatusSeeOther) - } - if checkCallback { - jwtToken, err1 := p.createJwtToken(user, room) - if err1 != nil { - mlog.Error("Error Create JWT context", mlog.Err(err1)) - http.Error(w, "Internal error", http.StatusInternalServerError) - } - redirectURL = redirectURL + jwtToken - http.Redirect(w, r, redirectURL, http.StatusSeeOther) - } else { - redirectURL = *p.API.GetConfig().ServiceSettings.SiteURL - http.Redirect(w, r, redirectURL, http.StatusSeeOther) + validJwtToken, err1 := p.createJwtToken(room) + if err1 != nil { + mlog.Error("Error Create JWT context", mlog.Err(err1)) + return redirectURL + "invalidjwttoken" } + + if p.callback.room == room { + return redirectURL + validJwtToken + } + return *p.API.GetConfig().ServiceSettings.SiteURL + } -func (p *Plugin) createJwtToken(user *model.User, room string) (string, error) { +func (p *Plugin) createJwtToken(room string) (string, error) { // Error check is done in configuration.IsValid() jURL, _ := url.Parse(p.getConfiguration().GetJitsiURL()) + user, err := p.API.GetUser(p.callback.UserID) + if err != nil { + mlog.Error("Error Get User", mlog.Err(err)) + return "", err + } + var meetingLinkValidUntil = time.Time{} meetingLinkValidUntil = time.Now().Add(time.Duration(p.getConfiguration().JitsiLinkValidTime) * time.Minute) @@ -396,12 +389,3 @@ func (p *Plugin) createJwtToken(user *model.User, room string) (string, error) { return signClaims(p.getConfiguration().JitsiAppSecret, &claims) } - -func (p *Plugin) checkValidationCallback(user *model.User, room string) (bool, error) { - if p.callback.room != room { - return false, nil - } else if p.callback.UserID != user.Id { - return false, nil - } - return true, nil -} diff --git a/server/api_test.go b/server/api_test.go index c9e74b80..6989011e 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -1,11 +1,72 @@ package main -import "testing" +import ( + "strings" + "testing" -func TestCallback(t *testing.T) { - // Case Success + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin/plugintest" + "github.com/stretchr/testify/require" +) - // Case Fail Authenticate +func TestRedirect(t *testing.T) { + p := Plugin{ + configuration: &configuration{ + JitsiURL: "http://test", + JitsiEmbedded: false, + JitsiNamingScheme: "mattermost", + JitsiAppSecret: "test-secret", + JitsiAppID: "test-appidjitsi", + }, + botID: "test-bot-id", + } + site := "http://mattermostserver" + servicesetting := model.ServiceSettings{SiteURL: &site} - // Case + t.Run("Redirect with Valid JWT", func(t *testing.T) { + apiMock := plugintest.API{} + defer apiMock.AssertExpectations(t) + p.SetAPI(&apiMock) + p.callback = CallbackValidation{ + room: "validroom", + UserID: "test-user"} + + apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user", Locale: "en"}, nil) + apiMock.On("GetConfig").Return(&model.Config{ServiceSettings: servicesetting}) + + result := p.handleRedirectURL("validroom") + require.True(t, strings.Contains(result, "validroom"), "Check Path should have room name") + require.True(t, strings.Contains(result, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"), "url has JWT header") + }) + + t.Run("Redirect to site url for login authentication", func(t *testing.T) { + apiMock := plugintest.API{} + defer apiMock.AssertExpectations(t) + p.SetAPI(&apiMock) + p.callback = CallbackValidation{ + room: "validroom", + UserID: "test-user"} + + apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user", Locale: "en"}, nil) + apiMock.On("GetConfig").Return(&model.Config{ServiceSettings: servicesetting}) + + result := p.handleRedirectURL("invalidroom") + require.Equal(t, site, result) + }) + + t.Run("Redirect with invalid JWT, error internal", func(t *testing.T) { + apiMock := plugintest.API{} + defer apiMock.AssertExpectations(t) + p.SetAPI(&apiMock) + p.callback = CallbackValidation{ + room: "validroom", + UserID: "test-user"} + apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user", Locale: "en"}, nil) + apiMock.On("GetConfig").Return(&model.Config{ServiceSettings: servicesetting}) + + p.configuration.JitsiAppSecret = "" // error if app secret is empty string + + result := p.handleRedirectURL("validroom") + require.True(t, strings.Contains(result, "invalidjwttoken"), "error internal, should send invalid jwt token") + }) }