[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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
105fa4a195
Коммит
a3b194581f
@@ -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
Обычный файл
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
Обычный файл
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
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user