[MM-37984] Allow Desktop App to authenticate via external providers outside of the app on supported servers (#24140)

* [MM-37984] Allow Desktop App to authenticate via external providers outside of the app on supported servers

* PR feedback

* Add support for mattermost-dev protocol for development use

* Update server/channels/db/migrations/postgres/000110_create_desktop_tokens.up.sql

* Fix silly typo

* Update server/channels/db/migrations/postgres/000110_create_desktop_tokens.up.sql

* Remove storage of client token, only validate it on the client

* Update migrations

* Add concurrently create index

* Remove CONCURRENTLY for now

* Fix issue with changing history

* Remove old migration

* Use idempotent statement to drop old index

* Remove reference to old table
Этот коммит содержится в:
Devin Binnie
2023-08-30 11:21:43 -04:00
коммит произвёл GitHub
родитель 105fa4a195
Коммит a3b194581f
41 изменённых файлов: 1569 добавлений и 24 удалений

Просмотреть файл

@@ -577,6 +577,7 @@ type AppIface interface {
FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError)
FindTeamByName(name string) bool
FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData)
GenerateAndSaveDesktopToken(expiryTime int64, user *model.User) (*string, *model.AppError)
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
GeneratePresignURLForExport(name string) (*model.PresignURLResponse, *model.AppError)
GeneratePublicLink(siteURL string, info *model.FileInfo) string
@@ -702,8 +703,8 @@ type AppIface interface {
GetOAuthAppsByCreator(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError)
GetOAuthCodeRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError)
GetOAuthImplicitRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError)
GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError)
GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError)
GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError)
GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string, desktopToken string) (string, *model.AppError)
GetOAuthStateToken(token string) (*model.Token, *model.AppError)
GetOnboarding() (*model.System, *model.AppError)
GetOpenGraphMetadata(requestURL string) ([]byte, error)
@@ -1169,6 +1170,7 @@ type AppIface interface {
UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
UserIsFirstAdmin(user *model.User) bool
ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError)
VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError
VerifyUserEmail(userID, email string) *model.AppError
ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError)

48
server/channels/app/desktop_login.go Обычный файл
Просмотреть файл

@@ -0,0 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost/server/public/model"
)
func (a *App) GenerateAndSaveDesktopToken(expiryTime int64, user *model.User) (*string, *model.AppError) {
token := model.NewRandomString(64)
err := a.Srv().Store().DesktopTokens().Insert(token, expiryTime, user.Id)
if err != nil {
// Delete any other related tokens if there's an error
a.Srv().Store().DesktopTokens().DeleteByUserId(user.Id)
return nil, model.NewAppError("GenerateAndSaveDesktopToken", "app.desktop_token.generateServerToken.invalid_or_expired", nil, err.Error(), http.StatusBadRequest)
}
return &token, nil
}
func (a *App) ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError) {
// Check if token is valid
userId, err := a.Srv().Store().DesktopTokens().GetUserId(token, expiryTime)
if err != nil {
// Delete the token if it is expired or invalid
a.Srv().Store().DesktopTokens().Delete(token)
return nil, model.NewAppError("ValidateDesktopToken", "app.desktop_token.validate.invalid", nil, err.Error(), http.StatusUnauthorized)
}
// Get the user profile
user, userErr := a.GetUser(*userId)
if userErr != nil {
// Delete the token if the user is invalid somehow
a.Srv().Store().DesktopTokens().Delete(token)
return nil, model.NewAppError("ValidateDesktopToken", "app.desktop_token.validate.no_user", nil, userErr.Error(), http.StatusInternalServerError)
}
// Clean up other tokens if they exist
a.Srv().Store().DesktopTokens().DeleteByUserId(*userId)
return user, nil
}

75
server/channels/app/desktop_login_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
TTL = time.Minute * 3
ExpiredLength = time.Minute * 10
)
func TestGenerateAndSaveDesktopToken(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("generate token", func(t *testing.T) {
token, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-TTL).Unix(), th.BasicUser)
assert.Nil(t, err)
assert.NotNil(t, token)
})
}
func TestValidateDesktopToken(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
authenticatedServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-TTL).Unix(), th.BasicUser)
require.Nil(t, err)
require.NotNil(t, authenticatedServerToken)
expiredServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-ExpiredLength).Unix(), th.BasicUser2)
require.Nil(t, err)
require.NotNil(t, expiredServerToken)
badUser := model.User{Id: "some_garbage_user_id"}
badUserServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-TTL).Unix(), &badUser)
require.Nil(t, err)
require.NotNil(t, badUserServerToken)
t.Run("validate token", func(t *testing.T) {
user, err := th.App.ValidateDesktopToken(*authenticatedServerToken, time.Now().Add(-TTL).Unix())
assert.Nil(t, err)
assert.NotNil(t, user)
assert.Equal(t, th.BasicUser.Id, user.Id)
})
t.Run("validate token - expired", func(t *testing.T) {
user, err := th.App.ValidateDesktopToken(*expiredServerToken, time.Now().Add(-TTL).Unix())
assert.NotNil(t, err)
assert.Nil(t, user)
assert.Equal(t, "app.desktop_token.validate.invalid", err.Id)
})
t.Run("validate token - not authenticated", func(t *testing.T) {
user, err := th.App.ValidateDesktopToken("not_real_token", time.Now().Add(-TTL).Unix())
assert.NotNil(t, err)
assert.Nil(t, user)
assert.Equal(t, "app.desktop_token.validate.invalid", err.Id)
})
t.Run("validate token - bad user id", func(t *testing.T) {
user, err := th.App.ValidateDesktopToken(*badUserServerToken, time.Now().Add(-TTL).Unix())
assert.NotNil(t, err)
assert.Nil(t, user)
assert.Equal(t, "app.desktop_token.validate.no_user", err.Id)
})
}

Просмотреть файл

@@ -429,7 +429,7 @@ func (a *App) newSessionUpdateToken(app *model.OAuthApp, accessData *model.Acces
return accessRsp, nil
}
func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError) {
func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = action
if teamID != "" {
@@ -440,6 +440,10 @@ func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, serv
stateProps["redirect_to"] = redirectTo
}
if desktopToken != "" {
stateProps["desktop_token"] = desktopToken
}
stateProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, loginHint)
@@ -450,13 +454,17 @@ func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, serv
return authURL, nil
}
func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError) {
func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string, desktopToken string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionSignup
if teamID != "" {
stateProps["team_id"] = teamID
}
if desktopToken != "" {
stateProps["desktop_token"] = desktopToken
}
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, "")
if err != nil {
return "", err

Просмотреть файл

@@ -4539,6 +4539,28 @@ func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64, p
a.app.FinishSendAdminNotifyPost(trial, now, pluginBasedData)
}
func (a *OpenTracingAppLayer) GenerateAndSaveDesktopToken(expiryTime int64, user *model.User) (*string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateAndSaveDesktopToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GenerateAndSaveDesktopToken(expiryTime, user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateMfaSecret")
@@ -7613,7 +7635,7 @@ func (a *OpenTracingAppLayer) GetOAuthImplicitRedirect(userID string, authReques
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, action string, redirectTo string, loginHint string, isMobile bool) (string, *model.AppError) {
func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, action string, redirectTo string, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthLoginEndpoint")
@@ -7625,7 +7647,7 @@ func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *ht
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthLoginEndpoint(w, r, service, teamID, action, redirectTo, loginHint, isMobile)
resultVar0, resultVar1 := a.app.GetOAuthLoginEndpoint(w, r, service, teamID, action, redirectTo, loginHint, isMobile, desktopToken)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
@@ -7635,7 +7657,7 @@ func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *ht
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string) (string, *model.AppError) {
func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, desktopToken string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthSignupEndpoint")
@@ -7647,7 +7669,7 @@ func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *h
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthSignupEndpoint(w, r, service, teamID)
resultVar0, resultVar1 := a.app.GetOAuthSignupEndpoint(w, r, service, teamID, desktopToken)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
@@ -18583,6 +18605,28 @@ func (a *OpenTracingAppLayer) UserIsInAdminRoleGroup(userID string, syncableID s
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateDesktopToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ValidateDesktopToken(token, expiryTime)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateUserPermissionsOnChannels")

Просмотреть файл

@@ -39,6 +39,7 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/audit"
"github.com/mattermost/mattermost/server/v8/channels/jobs"
"github.com/mattermost/mattermost/server/v8/channels/jobs/active_users"
"github.com/mattermost/mattermost/server/v8/channels/jobs/cleanup_desktop_tokens"
"github.com/mattermost/mattermost/server/v8/channels/jobs/expirynotify"
"github.com/mattermost/mattermost/server/v8/channels/jobs/export_delete"
"github.com/mattermost/mattermost/server/v8/channels/jobs/export_process"
@@ -1637,6 +1638,12 @@ func (s *Server) initJobs() {
hosted_purchase_screening.MakeScheduler(s.Jobs, s.License()),
)
s.Jobs.RegisterJobType(
model.JobTypeCleanupDesktopTokens,
cleanup_desktop_tokens.MakeWorker(s.Jobs, s.Store()),
cleanup_desktop_tokens.MakeScheduler(s.Jobs),
)
s.platform.Jobs = s.Jobs
}