From d414a7c4eec77e5df038fb441a4b0d834b712f88 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 12:58:24 +0200 Subject: [PATCH 01/20] Add ExternalAuthToken model Signed-off-by: Jarkko Lehtoranta --- models/auth/external_auth_token.go | 138 +++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 models/auth/external_auth_token.go diff --git a/models/auth/external_auth_token.go b/models/auth/external_auth_token.go new file mode 100644 index 0000000000000..7114af7f74154 --- /dev/null +++ b/models/auth/external_auth_token.go @@ -0,0 +1,138 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +type ErrExternalAuthTokenNotExist struct { + SessionID string + AuthTokenID string +} + +func IsErrExternalAuthTokenNotExist(err error) bool { + _, ok := err.(ErrExternalAuthTokenNotExist) + return ok +} + +func (err ErrExternalAuthTokenNotExist) Error() string { + return fmt.Sprintf("external auth token does not exist [sessionID: %s, authTokenID: %s]", err.SessionID, err.AuthTokenID) +} + +func (err ErrExternalAuthTokenNotExist) Unwrap() error { + return util.ErrNotExist +} + +type ExternalAuthToken struct { + SessionID string `xorm:"pk"` + AuthTokenID string `xorm:"INDEX"` + UserID int64 `xorm:"INDEX NOT NULL"` + ExternalID string `xorm:"NOT NULL"` + LoginSourceID int64 `xorm:"INDEX NOT NULL"` + RawData map[string]any `xorm:"TEXT JSON"` + AccessToken string `xorm:"TEXT"` + AccessTokenSecret string `xorm:"TEXT"` + RefreshToken string `xorm:"TEXT"` + ExpiresAt time.Time + IDToken string `xorm:"TEXT"` +} + +func init() { + db.RegisterModel(new(ExternalAuthToken)) +} + +func InsertExternalAuthToken(ctx context.Context, t *ExternalAuthToken) error { + _, err := db.GetEngine(ctx).Insert(t) + return err +} + +func GetExternalAuthTokenBySessionID(ctx context.Context, sessionID string) (*ExternalAuthToken, error) { + t := &ExternalAuthToken{} + has, err := db.GetEngine(ctx).ID(sessionID).Get(t) + if err != nil { + return nil, err + } + if !has { + return nil, ErrExternalAuthTokenNotExist{SessionID: sessionID} + } + return t, nil +} + +func GetExternalAuthTokenByAuthTokenID(ctx context.Context, authTokenID string) (*ExternalAuthToken, error) { + t := &ExternalAuthToken{} + has, err := db.GetEngine(ctx).Where(builder.Eq{"auth_token_id": authTokenID}).Get(t) + if err != nil { + return nil, err + } + if !has { + return nil, ErrExternalAuthTokenNotExist{AuthTokenID: authTokenID} + } + return t, nil +} + +func GetExternalAuthTokenSessionIDsAndAuthTokenIDs(ctx context.Context, userID, loginSourceID int64) ([]*ExternalAuthToken, error) { + tlist := []*ExternalAuthToken{} + cond := builder.NewCond().And(builder.Eq{"user_id": userID}) + if loginSourceID > 0 { + cond = cond.And(builder.Eq{"login_source_id": loginSourceID}) + } + if err := db.GetEngine(ctx).Cols("session_id", "auth_token_id").Where(cond).Find(&tlist); err != nil { + return nil, err + } + return tlist, nil +} + +func UpdateExternalAuthTokenBySessionID(ctx context.Context, sessionID string, t *ExternalAuthToken) error { + _, err := db.GetEngine(ctx).ID(sessionID).AllCols().Update(t) + return err +} + +func DeleteExternalAuthTokenBySessionID(ctx context.Context, sessionID string) error { + _, err := db.GetEngine(ctx).ID(sessionID).Delete(&ExternalAuthToken{}) + return err +} + +func DeleteExternalAuthTokensByUserLoginSourceID(ctx context.Context, userID, loginSourceID int64) error { + _, err := db.GetEngine(ctx).Where(builder.Eq{"user_id": userID, "login_source_id": loginSourceID}).Delete(&ExternalAuthToken{}) + return err +} + +func DeleteExternalAuthTokensByUserID(ctx context.Context, userID int64) error { + _, err := db.GetEngine(ctx).Where(builder.Eq{"user_id": userID}).Delete(&ExternalAuthToken{}) + return err +} + +type FindExternalAuthTokenOptions struct { + db.ListOptions + UserID int64 + ExternalID string + LoginSourceID int64 + OrderBy string +} + +func (opts FindExternalAuthTokenOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.UserID > 0 { + cond = cond.And(builder.Eq{"user_id": opts.UserID}) + } + if len(opts.ExternalID) > 0 { + cond = cond.And(builder.Eq{"external_id": opts.ExternalID}) + } + if opts.LoginSourceID > 0 { + cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID}) + } + return cond +} + +func (opts FindExternalAuthTokenOptions) ToOrders() string { + return opts.OrderBy +} From dc03aecce4fd1477213cb5f5d98e6182f1fc1310 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:02:16 +0200 Subject: [PATCH 02/20] Fix Exist function in virtual session provider Signed-off-by: Jarkko Lehtoranta --- modules/session/virtual.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/session/virtual.go b/modules/session/virtual.go index 80352b6e721de..9aaf39745e3c7 100644 --- a/modules/session/virtual.go +++ b/modules/session/virtual.go @@ -69,7 +69,9 @@ func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) { // Exist returns true if session with given ID exists. func (o *VirtualSessionProvider) Exist(sid string) bool { - return true + o.lock.RLock() + defer o.lock.RUnlock() + return o.provider.Exist(sid) } // Destroy deletes a session by session ID. From 1a648a3978cb0422843f70074f5160880666d080 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:02:43 +0200 Subject: [PATCH 03/20] Support accessing virtual session provider for managing existing sessions Signed-off-by: Jarkko Lehtoranta --- modules/session/provider.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 modules/session/provider.go diff --git a/modules/session/provider.go b/modules/session/provider.go new file mode 100644 index 0000000000000..539c90692fc67 --- /dev/null +++ b/modules/session/provider.go @@ -0,0 +1,16 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package session + +import ( + "code.gitea.io/gitea/modules/setting" +) + +func GetSessionProvider() (*VirtualSessionProvider, error) { + sessionProvider := &VirtualSessionProvider{} + if err := sessionProvider.Init(setting.SessionConfig.Gclifetime, setting.SessionConfig.ProviderConfig); err != nil { + return nil, err + } + return sessionProvider, nil +} From 658b9d6d6551404af8ecc61e16fcba4d0bc82d78 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:03:32 +0200 Subject: [PATCH 04/20] Add ExternalAuthToken service Signed-off-by: Jarkko Lehtoranta --- services/auth/external_auth_token.go | 98 ++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 services/auth/external_auth_token.go diff --git a/services/auth/external_auth_token.go b/services/auth/external_auth_token.go new file mode 100644 index 0000000000000..e1d83ae918a6d --- /dev/null +++ b/services/auth/external_auth_token.go @@ -0,0 +1,98 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "context" + + auth "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/session" + + "github.com/markbates/goth" +) + +func toExternalAuthToken(ctx context.Context, sessionID string, user *user_model.User, gothUser *goth.User) (*auth.ExternalAuthToken, error) { + authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider) + if err != nil { + return nil, err + } + return &auth.ExternalAuthToken{ + SessionID: sessionID, + UserID: user.ID, + ExternalID: gothUser.UserID, + LoginSourceID: authSource.ID, + RawData: gothUser.RawData, + AccessToken: gothUser.AccessToken, + AccessTokenSecret: gothUser.AccessTokenSecret, + RefreshToken: gothUser.RefreshToken, + ExpiresAt: gothUser.ExpiresAt, + IDToken: gothUser.IDToken, + }, nil +} + +func SetExternalAuthToken(ctx context.Context, sessionID string, user *user_model.User, gothUser *goth.User) error { + t, err := toExternalAuthToken(ctx, sessionID, user, gothUser) + if err != nil { + return err + } + + oldt, err := auth.GetExternalAuthTokenBySessionID(ctx, sessionID) + if auth.IsErrExternalAuthTokenNotExist(err) { + return auth.InsertExternalAuthToken(ctx, t) + } else if err != nil { + return err + } + + t.AuthTokenID = oldt.AuthTokenID + return auth.UpdateExternalAuthTokenBySessionID(ctx, sessionID, t) +} + +func UpdateExternalAuthTokenSessionID(ctx context.Context, oldSessionID, sessionID string) error { + t, err := auth.GetExternalAuthTokenBySessionID(ctx, oldSessionID) + if err != nil { + return err + } + t.SessionID = sessionID + return auth.UpdateExternalAuthTokenBySessionID(ctx, oldSessionID, t) +} + +func UpdateExternalAuthTokenSessionIDByAuthTokenID(ctx context.Context, authTokenID, sessionID string) error { + t, err := auth.GetExternalAuthTokenByAuthTokenID(ctx, authTokenID) + if err != nil { + return err + } + oldSessionID := t.SessionID + t.SessionID = sessionID + return auth.UpdateExternalAuthTokenBySessionID(ctx, oldSessionID, t) +} + +func UpdateExternalAuthTokenAuthTokenID(ctx context.Context, sessionID, authTokenID string) error { + t, err := auth.GetExternalAuthTokenBySessionID(ctx, sessionID) + if err != nil { + return err + } + t.AuthTokenID = authTokenID + return auth.UpdateExternalAuthTokenBySessionID(ctx, sessionID, t) +} + +func CleanExternalAuthTokensByUser(ctx context.Context, userID int64) error { + tokens, err := auth.GetExternalAuthTokenSessionIDsAndAuthTokenIDs(ctx, userID, 0) + if err != nil { + return err + } + sessionProvider, err := session.GetSessionProvider() + if err != nil { + return err + } + for _, t := range tokens { + if !sessionProvider.Exist(t.SessionID) && (len(t.AuthTokenID) == 0 || !auth.ExistAuthToken(ctx, t.AuthTokenID)) { + if err := auth.DeleteExternalAuthTokenBySessionID(ctx, t.SessionID); err != nil { + return err + } + } + } + + return nil +} From b3fa45d102b1fea32999e362bb4f3e6764da961a Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:04:57 +0200 Subject: [PATCH 05/20] Remove external session related data from ExternalLoginUser Signed-off-by: Jarkko Lehtoranta --- models/user/external_login_user.go | 30 ++++++++++++------------------ services/externalaccount/user.go | 29 ++++++++++++----------------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/models/user/external_login_user.go b/models/user/external_login_user.go index 965b7a5ed1dfd..e197cc1cc327f 100644 --- a/models/user/external_login_user.go +++ b/models/user/external_login_user.go @@ -6,7 +6,6 @@ package user import ( "context" "fmt" - "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/util" @@ -57,23 +56,18 @@ func (err ErrExternalLoginUserNotExist) Unwrap() error { // ExternalLoginUser makes the connecting between some existing user and additional external login sources type ExternalLoginUser struct { - ExternalID string `xorm:"pk NOT NULL"` - UserID int64 `xorm:"INDEX NOT NULL"` - LoginSourceID int64 `xorm:"pk NOT NULL"` - RawData map[string]any `xorm:"TEXT JSON"` - Provider string `xorm:"index VARCHAR(25)"` - Email string - Name string - FirstName string - LastName string - NickName string - Description string - AvatarURL string `xorm:"TEXT"` - Location string - AccessToken string `xorm:"TEXT"` - AccessTokenSecret string `xorm:"TEXT"` - RefreshToken string `xorm:"TEXT"` - ExpiresAt time.Time + ExternalID string `xorm:"pk NOT NULL"` + UserID int64 `xorm:"INDEX NOT NULL"` + LoginSourceID int64 `xorm:"pk NOT NULL"` + Provider string `xorm:"index VARCHAR(25)"` + Email string + Name string + FirstName string + LastName string + NickName string + Description string + AvatarURL string `xorm:"TEXT"` + Location string } type ExternalUserMigrated interface { diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go index e2de41da188e5..e5a56d4e3f065 100644 --- a/services/externalaccount/user.go +++ b/services/externalaccount/user.go @@ -22,23 +22,18 @@ func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser go return nil, err } return &user_model.ExternalLoginUser{ - ExternalID: gothUser.UserID, - UserID: user.ID, - LoginSourceID: authSource.ID, - RawData: gothUser.RawData, - Provider: gothUser.Provider, - Email: gothUser.Email, - Name: gothUser.Name, - FirstName: gothUser.FirstName, - LastName: gothUser.LastName, - NickName: gothUser.NickName, - Description: gothUser.Description, - AvatarURL: gothUser.AvatarURL, - Location: gothUser.Location, - AccessToken: gothUser.AccessToken, - AccessTokenSecret: gothUser.AccessTokenSecret, - RefreshToken: gothUser.RefreshToken, - ExpiresAt: gothUser.ExpiresAt, + ExternalID: gothUser.UserID, + UserID: user.ID, + LoginSourceID: authSource.ID, + Provider: gothUser.Provider, + Email: gothUser.Email, + Name: gothUser.Name, + FirstName: gothUser.FirstName, + LastName: gothUser.LastName, + NickName: gothUser.NickName, + Description: gothUser.Description, + AvatarURL: gothUser.AvatarURL, + Location: gothUser.Location, }, nil } From cf868225131f0cf654c23e4adea955ef9277788b Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:05:43 +0200 Subject: [PATCH 06/20] Migrate DB ExternalLoginUser by removing ExternalAuthToken related columns Signed-off-by: Jarkko Lehtoranta --- models/migrations/migrations.go | 2 ++ models/migrations/v1_22/v292.go | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 models/migrations/v1_22/v292.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 87fddefb8824a..2791bcc8f027f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -568,6 +568,8 @@ var migrations = []Migration{ NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable), // v291 -> v292 NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment), + // v292 -> v293 + NewMigration("Drop raw_data, access_token, access_token_secret, refresh_token and expires_at columns from external_login_user table", v1_22.DropColumnsFromExternalLoginUserTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_22/v292.go b/models/migrations/v1_22/v292.go new file mode 100644 index 0000000000000..06e370f18d6bb --- /dev/null +++ b/models/migrations/v1_22/v292.go @@ -0,0 +1,37 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "time" + + "code.gitea.io/gitea/models/migrations/base" + + "xorm.io/xorm" +) + +func DropColumnsFromExternalLoginUserTable(x *xorm.Engine) error { + type ExternalLoginUser struct { + RawData map[string]any `xorm:"TEXT JSON"` + AccessToken string `xorm:"TEXT"` + AccessTokenSecret string `xorm:"TEXT"` + RefreshToken string `xorm:"TEXT"` + ExpiresAt time.Time + } + if err := x.Sync(new(ExternalLoginUser)); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if err := base.DropTableColumns(sess, "external_login_user", "raw_data", "access_token", "access_token_secret", "refresh_token", "expires_at"); err != nil { + return err + } + + return sess.Commit() +} From 741193ae0d065e4ab02d06a72e040f52425502d1 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:06:48 +0200 Subject: [PATCH 07/20] Register auth source Type to gob Signed-off-by: Jarkko Lehtoranta --- models/auth/source.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/auth/source.go b/models/auth/source.go index f360ca98017d0..d73ce5e70607d 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -6,6 +6,7 @@ package auth import ( "context" + "encoding/gob" "fmt" "reflect" @@ -129,6 +130,7 @@ func (Source) TableName() string { func init() { db.RegisterModel(new(Source)) + gob.Register(Type(0)) } // BeforeSet is invoked from XORM before setting the value of a field of this object. From 266b4d950e4633457be4ddcc982b6cac3631c767 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:14:22 +0200 Subject: [PATCH 08/20] Use login type identifiers in a session Signed-off-by: Jarkko Lehtoranta --- routers/web/auth/auth.go | 4 ++++ routers/web/auth/linkaccount.go | 20 ++++++++++++++++++++ routers/web/auth/oauth.go | 6 ++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 8b5cd986b81a6..b82ff6fcbf2f0 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -314,6 +314,8 @@ func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) { } func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string { + loginType, _ := ctx.Session.Get("login_type").(auth.Type) + if remember { nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID) if err != nil { @@ -404,6 +406,8 @@ func HandleSignOut(ctx *context.Context) { // SignOut sign out from login status func SignOut(ctx *context.Context) { + loginType, _ := ctx.Session.Get("login_type").(auth.Type) + if ctx.Doer != nil { eventsource.GetManager().SendMessageBlocking(ctx.Doer.ID, &eventsource.Event{ Name: "logout", diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index f744a57a43f94..46e56f686a75d 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -174,6 +174,19 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r return } + source, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider) + if err != nil { + ctx.ServerError("UserLinkAccount", err) + return + } + + if err := ctx.Session.Set("login_source_id", source.ID); err != nil { + log.Error("UserLinkAccount: %v", err) + } + if err := ctx.Session.Set("login_type", source.Type); err != nil { + log.Error("UserLinkAccount: %v", err) + } + handleSignIn(ctx, u, remember) return } @@ -301,5 +314,12 @@ func LinkAccountPostRegister(ctx *context.Context) { return } + if err := ctx.Session.Set("login_source_id", authSource.ID); err != nil { + log.Error("UserSignUp: %v", err) + } + if err := ctx.Session.Set("login_type", authSource.Type); err != nil { + log.Error("UserSignUp: %v", err) + } + handleSignIn(ctx, u, false) } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 3189d1372e259..3a8e0a599798c 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1117,8 +1117,10 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model // we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page. if !needs2FA { if err := updateSession(ctx, nil, map[string]any{ - "uid": u.ID, - "uname": u.Name, + "uid": u.ID, + "uname": u.Name, + "login_source_id": source.ID, + "login_type": source.Type, }); err != nil { ctx.ServerError("updateSession", err) return From 207dc75978451d9f9cde3c8fbf812df16c1cce13 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:17:52 +0200 Subject: [PATCH 09/20] Add login type identifiers to AuthToken Signed-off-by: Jarkko Lehtoranta --- models/auth/auth_token.go | 11 +++++++---- routers/install/install.go | 2 +- services/auth/auth_token.go | 18 +++++++++++------- services/auth/auth_token_test.go | 8 ++++---- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/models/auth/auth_token.go b/models/auth/auth_token.go index 81f07d1a8382c..ceeb00b0fad11 100644 --- a/models/auth/auth_token.go +++ b/models/auth/auth_token.go @@ -16,10 +16,13 @@ import ( var ErrAuthTokenNotExist = util.NewNotExistErrorf("auth token does not exist") type AuthToken struct { //nolint:revive - ID string `xorm:"pk"` - TokenHash string - UserID int64 `xorm:"INDEX"` - ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"` + ID string `xorm:"pk"` + TokenHash string + UserID int64 `xorm:"INDEX"` + ExternalID string + LoginSourceID int64 + LoginType Type + ExpiresUnix timeutil.TimeStamp `xorm:"INDEX"` } func init() { diff --git a/routers/install/install.go b/routers/install/install.go index 9c6a8849b63ff..f1e4171fd6895 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -555,7 +555,7 @@ func SubmitInstall(ctx *context.Context) { u, _ = user_model.GetUserByName(ctx, u.Name) } - nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID) + nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID, 0, 0) if err != nil { ctx.ServerError("CreateAuthTokenForUserID", err) return diff --git a/services/auth/auth_token.go b/services/auth/auth_token.go index 6b59238c984c5..86802287789af 100644 --- a/services/auth/auth_token.go +++ b/services/auth/auth_token.go @@ -70,10 +70,12 @@ func RegenerateAuthToken(ctx context.Context, t *auth_model.AuthToken) (*auth_mo } newToken := &auth_model.AuthToken{ - ID: t.ID, - TokenHash: hash, - UserID: t.UserID, - ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour), + ID: t.ID, + TokenHash: hash, + UserID: t.UserID, + LoginSourceID: t.LoginSourceID, + LoginType: t.LoginType, + ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour), } if err := auth_model.UpdateAuthTokenByID(ctx, newToken); err != nil { @@ -83,10 +85,12 @@ func RegenerateAuthToken(ctx context.Context, t *auth_model.AuthToken) (*auth_mo return newToken, token, nil } -func CreateAuthTokenForUserID(ctx context.Context, userID int64) (*auth_model.AuthToken, string, error) { +func CreateAuthTokenForUserID(ctx context.Context, userID, loginSourceID int64, loginType auth_model.Type) (*auth_model.AuthToken, string, error) { t := &auth_model.AuthToken{ - UserID: userID, - ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour), + UserID: userID, + LoginSourceID: loginSourceID, + LoginType: loginType, + ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour), } var err error diff --git a/services/auth/auth_token_test.go b/services/auth/auth_token_test.go index 23c8d17e59710..1037dfabd99b3 100644 --- a/services/auth/auth_token_test.go +++ b/services/auth/auth_token_test.go @@ -39,7 +39,7 @@ func TestCheckAuthToken(t *testing.T) { t.Run("Expired", func(t *testing.T) { timeutil.MockSet(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) - at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2) + at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2, 0, 0) assert.NoError(t, err) assert.NotNil(t, at) assert.NotEmpty(t, token) @@ -54,7 +54,7 @@ func TestCheckAuthToken(t *testing.T) { }) t.Run("InvalidHash", func(t *testing.T) { - at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2) + at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2, 0, 0) assert.NoError(t, err) assert.NotNil(t, at) assert.NotEmpty(t, token) @@ -67,7 +67,7 @@ func TestCheckAuthToken(t *testing.T) { }) t.Run("Valid", func(t *testing.T) { - at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2) + at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2, 0, 0) assert.NoError(t, err) assert.NotNil(t, at) assert.NotEmpty(t, token) @@ -86,7 +86,7 @@ func TestRegenerateAuthToken(t *testing.T) { timeutil.MockSet(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) defer timeutil.MockUnset() - at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2) + at, token, err := CreateAuthTokenForUserID(db.DefaultContext, 2, 0, 0) assert.NoError(t, err) assert.NotNil(t, at) assert.NotEmpty(t, token) From d6050eab1ce15ff70a3cdffd4b301a304499e70e Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:18:34 +0200 Subject: [PATCH 10/20] Support checking if an auth token exists Signed-off-by: Jarkko Lehtoranta --- models/auth/auth_token.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/models/auth/auth_token.go b/models/auth/auth_token.go index ceeb00b0fad11..ab0d282aee463 100644 --- a/models/auth/auth_token.go +++ b/models/auth/auth_token.go @@ -34,6 +34,14 @@ func InsertAuthToken(ctx context.Context, t *AuthToken) error { return err } +func ExistAuthToken(ctx context.Context, id string) bool { + exist, err := db.Exist[AuthToken](ctx, builder.Eq{"`id`": id}) + if err != nil { + return false + } + return exist +} + func GetAuthTokenByID(ctx context.Context, id string) (*AuthToken, error) { at := &AuthToken{} From 4073ff76421341f684bf35dc9ce218cc7e5e2412 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:22:32 +0200 Subject: [PATCH 11/20] Sync login type identifiers between an auth token and session Signed-off-by: Jarkko Lehtoranta --- routers/web/auth/auth.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index b82ff6fcbf2f0..3bad74dae4f50 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -88,8 +88,10 @@ func autoSignIn(ctx *context.Context) (bool, error) { if err := updateSession(ctx, nil, map[string]any{ // Set session IDs - "uid": u.ID, - "uname": u.Name, + "uid": u.ID, + "uname": u.Name, + "login_source_id": nt.LoginSourceID, + "login_type": nt.LoginType, }); err != nil { return false, fmt.Errorf("unable to updateSession: %w", err) } @@ -317,7 +319,9 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe loginType, _ := ctx.Session.Get("login_type").(auth.Type) if remember { - nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID) + loginSourceID, _ := ctx.Session.Get("login_source_id").(int64) + + nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID, loginSourceID, loginType) if err != nil { ctx.ServerError("CreateAuthTokenForUserID", err) return setting.AppSubURL + "/" From e9a50a662072fa625f27d44a344a022c7fd007ec Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:26:41 +0200 Subject: [PATCH 12/20] Manage external sessions by using ExternalAuthTokens Signed-off-by: Jarkko Lehtoranta --- routers/web/auth/auth.go | 22 ++++++++++++++++++++++ routers/web/auth/linkaccount.go | 5 +++++ routers/web/auth/oauth.go | 8 ++++++++ 3 files changed, 35 insertions(+) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 3bad74dae4f50..18a6f796ff721 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -96,6 +96,12 @@ func autoSignIn(ctx *context.Context) (bool, error) { return false, fmt.Errorf("unable to updateSession: %w", err) } + if nt.LoginType == auth.OAuth2 { + if err := auth_service.UpdateExternalAuthTokenSessionIDByAuthTokenID(ctx, nt.ID, ctx.Session.ID()); err != nil { + log.Error("UpdateExternalAuthTokenSessionIDByAuthTokenID: %v", err) + } + } + if err := resetLocale(ctx, u); err != nil { return false, err } @@ -327,9 +333,16 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe return setting.AppSubURL + "/" } + if loginType == auth.OAuth2 { + if err := auth_service.UpdateExternalAuthTokenAuthTokenID(ctx, ctx.Session.ID(), nt.ID); err != nil { + log.Error("UpdateExternalAuthTokenAuthTokenID: %v", err) + } + } + ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day) } + oldSessionID := ctx.Session.ID() if err := updateSession(ctx, []string{ // Delete the openid, 2fa and linkaccount data "openid_verified_uri", @@ -347,6 +360,12 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe return setting.AppSubURL + "/" } + if loginType == auth.OAuth2 { + if err := auth_service.UpdateExternalAuthTokenSessionID(ctx, oldSessionID, ctx.Session.ID()); err != nil { + log.Error("UpdateExternalAuthTokenSessionID: %v", err) + } + } + // Language setting of the user overwrites the one previously set // If the user does not have a locale set, we save the current one. if u.Language == "" { @@ -628,6 +647,9 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. log.Error("UpdateExternalUser failed: %v", err) } } + if err := auth_service.SetExternalAuthToken(ctx, ctx.Session.ID(), u, gothUser); err != nil { + log.Error("SetExternalAuthToken failed: %v", err) + } } // for active user or the first (admin) user, we don't need to send confirmation email diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index 46e56f686a75d..b5b57ca7a9d4a 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -187,6 +187,11 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r log.Error("UserLinkAccount: %v", err) } + if err := auth_service.SetExternalAuthToken(ctx, ctx.Session.ID(), u, &gothUser); err != nil { + ctx.ServerError("SetExternalAuthToken", err) + return + } + handleSignIn(ctx, u, remember) return } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 3a8e0a599798c..18e1bc58a24ef 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1152,6 +1152,14 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model } } + if err := auth_service.CleanExternalAuthTokensByUser(ctx, u.ID); err != nil { + log.Error("CleanUserExternalAuthTokens failed: %v", err) + } + + if err := auth_service.SetExternalAuthToken(ctx, ctx.Session.ID(), u, &gothUser); err != nil { + log.Error("SetExternalAuthToken failed: %v", err) + } + if err := resetLocale(ctx, u); err != nil { ctx.ServerError("resetLocale", err) return From 991e6822bcab6119c174f05ed182333effbcd4c0 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:27:32 +0200 Subject: [PATCH 13/20] Update from local sign in to OAuth2/OIDC sign in after linking an account Signed-off-by: Jarkko Lehtoranta --- routers/web/auth/oauth.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 18e1bc58a24ef..a2904cb7e03fd 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -941,6 +941,22 @@ func SignInOAuthCallback(ctx *context.Context) { return } + // update from local sign in to OAuth2 sign in + loginType, _ := ctx.Session.Get("login_type").(auth.Type) + if loginType <= auth.Plain { + if err := updateSession(ctx, nil, map[string]any{ + "login_source_id": authSource.ID, + "login_type": authSource.Type, + }); err != nil { + ctx.ServerError("updateSession", err) + return + } + } + + if err := auth_service.SetExternalAuthToken(ctx, ctx.Session.ID(), ctx.Doer, &gothUser); err != nil { + log.Error("SetExternalAuthToken failed: %v", err) + } + ctx.Redirect(setting.AppSubURL + "/user/settings/security") return } else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration { From b12cea118b146adbb24a6d450c19327ed6628156 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:28:42 +0200 Subject: [PATCH 14/20] Delete ExternalAuthTokens when removing an account link Signed-off-by: Jarkko Lehtoranta --- routers/web/user/setting/security/security.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go index 8d6859ab87da9..d6fe3f71d50b5 100644 --- a/routers/web/user/setting/security/security.go +++ b/routers/web/user/setting/security/security.go @@ -44,7 +44,9 @@ func DeleteAccountLink(ctx *context.Context) { if id <= 0 { ctx.Flash.Error("Account link id is not given") } else { - if _, err := user_model.RemoveAccountLink(ctx, ctx.Doer, id); err != nil { + if err := auth_model.DeleteExternalAuthTokensByUserLoginSourceID(ctx, ctx.Doer.ID, id); err != nil { + ctx.Flash.Error("DeleteExternalAuthTokens: " + err.Error()) + } else if _, err := user_model.RemoveAccountLink(ctx, ctx.Doer, id); err != nil { ctx.Flash.Error("RemoveAccountLink: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) From d12acc1155efe70a16625239aacc8f4b479a45c7 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:29:21 +0200 Subject: [PATCH 15/20] Delete ExternalAuthTokens when removing a user Signed-off-by: Jarkko Lehtoranta --- services/user/delete.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/user/delete.go b/services/user/delete.go index 212cb83e03114..a414fc4f58671 100644 --- a/services/user/delete.go +++ b/services/user/delete.go @@ -189,6 +189,12 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) } // ***** END: ExternalLoginUser ***** + // ***** START: ExternalAuthToken ***** + if err = auth_model.DeleteExternalAuthTokensByUserID(ctx, u.ID); err != nil { + return fmt.Errorf("DeleteExternalAuthTokensByUserID: %w", err) + } + // ***** END: ExternalAuthToken ***** + if err := auth_model.DeleteAuthTokensByUserID(ctx, u.ID); err != nil { return fmt.Errorf("DeleteAuthTokensByUserID: %w", err) } From 5296f7cec541d2472eddf7bda35d361d0dd16750 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:30:00 +0200 Subject: [PATCH 16/20] Generate OIDC RP-initiated logout URLs Signed-off-by: Jarkko Lehtoranta --- .../auth/source/oauth2/source_endsession.go | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 services/auth/source/oauth2/source_endsession.go diff --git a/services/auth/source/oauth2/source_endsession.go b/services/auth/source/oauth2/source_endsession.go new file mode 100644 index 0000000000000..e15d0c02b05fa --- /dev/null +++ b/services/auth/source/oauth2/source_endsession.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package oauth2 + +import ( + "encoding/base64" + "fmt" + "net/url" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + + "github.com/markbates/goth" + "github.com/markbates/goth/providers/openidConnect" +) + +// EndSessionEndpoint returns end_session_endpoint URI for OIDC sources +func (source *Source) EndSessionEndpoint(ctx *context.Context) (string, string, error) { + redirect := &url.URL{} + state := "" + providerName := source.authSource.Name + + gothProvider, err := goth.GetProvider(providerName) + if err != nil { + return "", "", err + } + + oidcProvider, ok := gothProvider.(*openidConnect.Provider) + if ok && oidcProvider.OpenIDConfig != nil && len(oidcProvider.OpenIDConfig.EndSessionEndpoint) > 0 { + if redirect, err = url.Parse(oidcProvider.OpenIDConfig.EndSessionEndpoint); err != nil { + return "", "", err + } + + r, err := util.CryptoRandomBytes(8) + if err != nil { + return "", "", err + } + state = base64.RawURLEncoding.EncodeToString(r) + + values := url.Values{} + values.Set("client_id", oidcProvider.ClientKey) + values.Set("post_logout_redirect_uri", fmt.Sprintf("%suser/oauth2/%s/logout/callback", setting.AppURL, providerName)) + values.Set("state", state) + + if ctx.Doer != nil { + t, err := auth_model.GetExternalAuthTokenBySessionID(ctx, ctx.Session.ID()) + if auth_model.IsErrExternalAuthTokenNotExist(err) { + log.Error("EndSessionEndpoint: %v", err) + } else if err != nil { + return "", "", err + } else if t.UserID == ctx.Doer.ID && len(t.IDToken) > 0 { + values.Set("id_token_hint", t.IDToken) + } else { + log.Error("EndSessionEndpoint IDToken missing for UserID %d [SessionID: %s, Provider: %s]", ctx.Doer.ID, ctx.Session.ID(), providerName) + } + } + + redirect.RawQuery = values.Encode() + } + + return redirect.String(), state, nil +} From ff17f0e0d967cf42bed622480316e94f388e172a Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:35:02 +0200 Subject: [PATCH 17/20] Add a handler for OAuth2 or OIDC RP-initiated logout Signed-off-by: Jarkko Lehtoranta --- routers/web/auth/oauth.go | 63 +++++++++++++++++++++++++++++++++++++++ routers/web/web.go | 1 + 2 files changed, 64 insertions(+) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index a2904cb7e03fd..cb00bbf6bc0f7 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -22,6 +22,7 @@ import ( auth_module "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -1023,6 +1024,68 @@ func SignInOAuthCallback(ctx *context.Context) { handleOAuth2SignIn(ctx, authSource, u, gothUser) } +// SignOutOAuth handles OAuth2 and OIDC RP-initiated logout requests +func SignOutOAuth(ctx *context.Context) { + provider := ctx.Params(":provider") + signOutErrMsg := ctx.Tr("Failed to sign out of %s, please sign out at your ID provider", provider) + + authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider) + if err != nil { + HandleSignOut(ctx) + ctx.Flash.Error(ctx.Tr("Failed to handle OAuth2 or OpenID Connect sign out"), true) + ctx.ServerError(fmt.Sprintf("SignOutOAuth[%s]", provider), err) + return + } + oauth2Source := authSource.Cfg.(*oauth2.Source) + redirect, state, err := oauth2Source.EndSessionEndpoint(ctx) + if err != nil { + HandleSignOut(ctx) + ctx.Flash.Error(signOutErrMsg, true) + ctx.ServerError(fmt.Sprintf("SignOutOAuth[%s]", provider), err) + return + } + + if ctx.Doer != nil { + eventsource.GetManager().SendMessageBlocking(ctx.Doer.ID, &eventsource.Event{ + Name: "logout", + Data: ctx.Session.ID(), + }) + } + + if err := auth.DeleteExternalAuthTokenBySessionID(ctx, ctx.Session.ID()); err != nil { + log.Error("DeleteExternalAuthTokenBySessionID: %v", err) + } + + // Sign out and redirect to landing page, if oidc end_session_endpoint was not found + if len(redirect) == 0 { + HandleSignOut(ctx) + ctx.Redirect(setting.AppSubURL + "/") + return + } + + // The user might not return via oidc callback + // Sign out, but keep the session alive for tracking oidc state + if err := ctx.Session.Flush(); err != nil { + HandleSignOut(ctx) + ctx.Flash.Error(signOutErrMsg, true) + ctx.ServerError(fmt.Sprintf("SignOutOAuth[%s]", provider), err) + return + } + if err := ctx.Session.Set("oidc_state", state); err != nil { + log.Error("SignOutOAuth[%s]: %v", provider, err) + } + if err := ctx.Session.Release(); err != nil { + HandleSignOut(ctx) + ctx.Flash.Error(signOutErrMsg, true) + ctx.ServerError(fmt.Sprintf("SignOutOAuth[%s]", provider), err) + return + } + ctx.DeleteSiteCookie(setting.CookieRememberName) + + // Redirect to oidc end_session_endpoint + ctx.Redirect(redirect) +} + func claimValueToStringSet(claimValue any) container.Set[string] { var groups []string diff --git a/routers/web/web.go b/routers/web/web.go index 3d790d16210ed..4cfa6680ad54a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -670,6 +670,7 @@ func registerRoutes(m *web.Route) { m.Group("/oauth2", func() { m.Get("/{provider}", auth.SignInOAuth) m.Get("/{provider}/callback", auth.SignInOAuthCallback) + m.Get("/{provider}/logout", auth.SignOutOAuth) }) }) // ***** END: User ***** From dc74954c8643e72a9292042cfa7a62478177a44e Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:36:54 +0200 Subject: [PATCH 18/20] Add a callback handler for OIDC RP-initiated logout Signed-off-by: Jarkko Lehtoranta --- routers/web/auth/oauth.go | 16 ++++++++++++++++ routers/web/web.go | 1 + 2 files changed, 17 insertions(+) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index cb00bbf6bc0f7..fee62062350ec 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1086,6 +1086,22 @@ func SignOutOAuth(ctx *context.Context) { ctx.Redirect(redirect) } +// SignOutOAuthCallback handles OIDC RP-initiated logout callback requests +func SignOutOAuthCallback(ctx *context.Context) { + provider := ctx.Params(":provider") + oidcState, _ := ctx.Session.Get("oidc_state").(string) + state := ctx.Req.URL.Query().Get("state") + + // Cleanup and destroy the remains of the old session + HandleSignOut(ctx) + if len(oidcState) == 0 || state != oidcState { + ctx.Flash.Error(ctx.Tr("Invalid state parameter, please check the %s sign out URL", provider), true) + ctx.ServerError(fmt.Sprintf("SignOutOAuthCallback[%s]", provider), fmt.Errorf("oidc_state: \"%s\", IdP state: \"%s\"", oidcState, state)) + return + } + ctx.Redirect(setting.AppSubURL + "/") +} + func claimValueToStringSet(claimValue any) container.Set[string] { var groups []string diff --git a/routers/web/web.go b/routers/web/web.go index 4cfa6680ad54a..ffb0308044e96 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -671,6 +671,7 @@ func registerRoutes(m *web.Route) { m.Get("/{provider}", auth.SignInOAuth) m.Get("/{provider}/callback", auth.SignInOAuthCallback) m.Get("/{provider}/logout", auth.SignOutOAuth) + m.Get("/{provider}/logout/callback", auth.SignOutOAuthCallback) }) }) // ***** END: User ***** From 041bb96616962be7f39ad8d6617513f11497818f Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:37:58 +0200 Subject: [PATCH 19/20] Show any sign out errors occurring before SignOutOAuth Signed-off-by: Jarkko Lehtoranta --- routers/web/auth/oauth.go | 7 +++++++ routers/web/web.go | 1 + 2 files changed, 8 insertions(+) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index fee62062350ec..a8df280d7595a 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1024,6 +1024,13 @@ func SignInOAuthCallback(ctx *context.Context) { handleOAuth2SignIn(ctx, authSource, u, gothUser) } +// SignOutOAuthError shows any sign out errors occurring before SignOutOAuth +func SignOutOAuthError(ctx *context.Context) { + HandleSignOut(ctx) + ctx.Flash.Error(ctx.Tr("Failed to handle OAuth2 or OpenID Connect sign out"), true) + ctx.ServerError("SignOutOAuthError", fmt.Errorf("error generating the internal OAuth2 logout URL")) +} + // SignOutOAuth handles OAuth2 and OIDC RP-initiated logout requests func SignOutOAuth(ctx *context.Context) { provider := ctx.Params(":provider") diff --git a/routers/web/web.go b/routers/web/web.go index ffb0308044e96..5325c8376b39e 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -672,6 +672,7 @@ func registerRoutes(m *web.Route) { m.Get("/{provider}/callback", auth.SignInOAuthCallback) m.Get("/{provider}/logout", auth.SignOutOAuth) m.Get("/{provider}/logout/callback", auth.SignOutOAuthCallback) + m.Get("/{provider}/logout/error", auth.SignOutOAuthError) }) }) // ***** END: User ***** From 514fa96c8bac7077cb78f41a9d256c016a523e00 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Mon, 25 Mar 2024 13:39:17 +0200 Subject: [PATCH 20/20] Redirect OAuth2/OIDC sessions to OAuth2/OIDC logout handler Signed-off-by: Jarkko Lehtoranta --- routers/web/auth/auth.go | 7 +++++++ routers/web/auth/oauth.go | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 18a6f796ff721..d2468138d352e 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -431,12 +431,19 @@ func HandleSignOut(ctx *context.Context) { func SignOut(ctx *context.Context) { loginType, _ := ctx.Session.Get("login_type").(auth.Type) + if loginType == auth.OAuth2 { + // Handle sign out in SignOutOAuth + redirectToSignOutOAuth(ctx) + return + } + if ctx.Doer != nil { eventsource.GetManager().SendMessageBlocking(ctx.Doer.ID, &eventsource.Event{ Name: "logout", Data: ctx.Session.ID(), }) } + HandleSignOut(ctx) ctx.JSONRedirect(setting.AppSubURL + "/") } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index a8df280d7595a..33697e43c375d 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1024,6 +1024,25 @@ func SignInOAuthCallback(ctx *context.Context) { handleOAuth2SignIn(ctx, authSource, u, gothUser) } +func redirectToSignOutOAuth(ctx *context.Context) { + errURL := "/user/oauth2/-/logout/error" + + loginSourceID, ok := ctx.Session.Get("login_source_id").(int64) + if !ok { + log.Error("redirectToSignOutOAuth: Failed to get login_source_id from session data") + ctx.JSONRedirect(errURL) + return + } + source, err := auth.GetSourceByID(ctx, loginSourceID) + if err != nil { + log.Error("redirectToSignOutOAuth: %w", err) + ctx.JSONRedirect(errURL) + return + } + + ctx.JSONRedirect("/user/oauth2/" + source.Name + "/logout") +} + // SignOutOAuthError shows any sign out errors occurring before SignOutOAuth func SignOutOAuthError(ctx *context.Context) { HandleSignOut(ctx)