From 7aacbd02456cd9043ac11ab5c09af691536fbfc6 Mon Sep 17 00:00:00 2001 From: wucm667 <109257021+wucm667@users.noreply.github.com> Date: Sat, 16 May 2026 05:25:40 +0800 Subject: [PATCH] fix(oidc): delete refresh tokens on end-session to prevent reuse after logout (#1458) Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Signed-off-by: wucm667 --- backend/internal/service/oidc_service.go | 14 +++- backend/internal/service/oidc_service_test.go | 77 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index e493a5b9..fbce6f81 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -1218,9 +1218,12 @@ func (s *OidcService) ValidateEndSession(ctx context.Context, input dto.OidcLogo return "", &common.OidcClientIdNotMatchingError{} } + tx := s.db.Begin() + defer tx.Rollback() + // Check if the user has authorized the client before var userAuthorizedOIDCClient model.UserAuthorizedOidcClient - err = s.db. + err = tx. WithContext(ctx). Preload("Client"). First(&userAuthorizedOIDCClient, "client_id = ? AND user_id = ?", clientID[0], userID). @@ -1229,6 +1232,11 @@ func (s *OidcService) ValidateEndSession(ctx context.Context, input dto.OidcLogo return "", &common.OidcMissingAuthorizationError{} } + // Delete all refresh tokens for this user + if err := tx.WithContext(ctx).Where("user_id = ?", userID).Delete(&model.OidcRefreshToken{}).Error; err != nil { + return "", err + } + // If the client has no logout callback URLs, return an error if len(userAuthorizedOIDCClient.Client.LogoutCallbackURLs) == 0 { return "", &common.OidcNoCallbackURLError{} @@ -1239,6 +1247,10 @@ func (s *OidcService) ValidateEndSession(ctx context.Context, input dto.OidcLogo return "", err } + if err := tx.Commit().Error; err != nil { + return "", fmt.Errorf("failed to commit transaction: %w", err) + } + return callbackURL, nil } diff --git a/backend/internal/service/oidc_service_test.go b/backend/internal/service/oidc_service_test.go index 40c7fcc7..bd5e663d 100644 --- a/backend/internal/service/oidc_service_test.go +++ b/backend/internal/service/oidc_service_test.go @@ -1249,3 +1249,80 @@ func TestPromptParameterConflicts(t *testing.T) { }) } } + +func TestOidcService_ValidateEndSession_DeletesRefreshTokens(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + common.EnvConfig.EncryptionKey = []byte("0123456789abcdef0123456789abcdef") + + mockConfig := NewTestAppConfigService(&model.AppConfig{ + SessionDuration: model.AppConfigVariable{Value: "60"}, + }) + mockJwtService, err := NewJwtService(t.Context(), db, mockConfig) + require.NoError(t, err) + + oidcService := &OidcService{ + db: db, + jwtService: mockJwtService, + } + + ctx := context.Background() + userID := "test-user-123" + clientID := "test-client-456" + + userEmail := "test@example.com" + user := model.User{ + Base: model.Base{ID: userID}, + Email: &userEmail, + } + require.NoError(t, db.Create(&user).Error) + + client := model.OidcClient{ + Base: model.Base{ID: clientID}, + Name: "Test Client", + LogoutCallbackURLs: []string{"https://example.com/logout"}, + } + require.NoError(t, db.Create(&client).Error) + + authorization := model.UserAuthorizedOidcClient{ + UserID: userID, + ClientID: clientID, + } + require.NoError(t, db.Create(&authorization).Error) + + refreshToken1 := model.OidcRefreshToken{ + Token: "refresh-token-1", + UserID: userID, + ClientID: clientID, + } + require.NoError(t, db.Create(&refreshToken1).Error) + + refreshToken2 := model.OidcRefreshToken{ + Token: "refresh-token-2", + UserID: userID, + ClientID: "other-client", + } + require.NoError(t, db.Create(&refreshToken2).Error) + + userClaims := map[string]any{ + "sub": userID, + "name": "Test User", + "email": userEmail, + } + idToken, err := mockJwtService.GenerateIDToken(userClaims, clientID, "", "") + require.NoError(t, err) + + input := dto.OidcLogoutDto{ + IdTokenHint: idToken, + ClientId: clientID, + PostLogoutRedirectUri: "https://example.com/logout", + } + + callbackURL, err := oidcService.ValidateEndSession(ctx, input, userID) + require.NoError(t, err) + assert.Equal(t, "https://example.com/logout", callbackURL) + + var remainingTokens []model.OidcRefreshToken + err = db.Where("user_id = ?", userID).Find(&remainingTokens).Error + require.NoError(t, err) + assert.Empty(t, remainingTokens, "all refresh tokens for the user should be deleted") +}