From 16a93b5ca8e51cd8a11b2a06aa2990d17bca662a Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Wed, 12 Nov 2025 12:27:53 +0000 Subject: [PATCH] Add ability to set cookie prefix for session cookies ( #87 ) --- audience_validation_test.go | 2 +- azure_oidc_test.go | 2 +- config/unified_config.go | 11 +-- csrf_session_test.go | 16 ++-- main.go | 2 +- main_servehttp_test.go | 2 +- main_test.go | 8 +- memory_leak_consolidated_test.go | 5 ++ profiling_test.go | 4 +- regression/regression_test.go | 6 +- security_edge_cases_test.go | 4 +- session.go | 10 ++- session/core/cookie_prefix_test.go | 127 +++++++++++++++++++++++++++ session/core/session_manager.go | 20 ++++- session/core/session_manager_test.go | 28 +++--- session_helpers_test.go | 6 +- session_test.go | 32 +++---- settings.go | 1 + test_framework_test.go | 2 + test_helpers_adapter_test.go | 4 +- token_consolidated_test.go | 6 +- 21 files changed, 228 insertions(+), 70 deletions(-) create mode 100644 session/core/cookie_prefix_test.go diff --git a/audience_validation_test.go b/audience_validation_test.go index ec2226b..7f445c1 100644 --- a/audience_validation_test.go +++ b/audience_validation_test.go @@ -838,7 +838,7 @@ func TestAudienceEndToEndScenario(t *testing.T) { } logger := NewLogger("debug") - sm, err := NewSessionManager(strings.Repeat("a", MinSessionEncryptionKeyLength), false, "", logger) + sm, err := NewSessionManager(strings.Repeat("a", MinSessionEncryptionKeyLength), false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } diff --git a/azure_oidc_test.go b/azure_oidc_test.go index 83e0668..34ed476 100644 --- a/azure_oidc_test.go +++ b/azure_oidc_test.go @@ -79,7 +79,7 @@ func TestAzureOIDCRegression(t *testing.T) { tOidc := &mockTraefikOidc{TraefikOidc: baseOidc} // Initialize session manager - sessionManager, _ := NewSessionManager("test-encryption-key-32-bytes-long", false, "", mockLogger) + sessionManager, _ := NewSessionManager("test-encryption-key-32-bytes-long", false, "", "", mockLogger) tOidc.sessionManager = sessionManager // Mock the JWT verification to avoid JWKS lookup issues diff --git a/config/unified_config.go b/config/unified_config.go index 836c0c8..5d82cae 100644 --- a/config/unified_config.go +++ b/config/unified_config.go @@ -69,11 +69,12 @@ type SessionConfig struct { MaxChunks int `json:"maxChunks" yaml:"maxChunks"` // Cookie settings - Domain string `json:"domain" yaml:"domain"` - Path string `json:"path" yaml:"path"` - Secure bool `json:"secure" yaml:"secure"` - HttpOnly bool `json:"httpOnly" yaml:"httpOnly"` - SameSite string `json:"sameSite" yaml:"sameSite"` + Domain string `json:"domain" yaml:"domain"` + Path string `json:"path" yaml:"path"` + Secure bool `json:"secure" yaml:"secure"` + HttpOnly bool `json:"httpOnly" yaml:"httpOnly"` + SameSite string `json:"sameSite" yaml:"sameSite"` + CookiePrefix string `json:"cookiePrefix" yaml:"cookiePrefix"` // Prefix for cookie names (e.g., "_oidc_myapp_") // Storage settings StorageType string `json:"storageType" yaml:"storageType"` // "memory", "redis", "cookie" diff --git a/csrf_session_test.go b/csrf_session_test.go index 8030072..790afd2 100644 --- a/csrf_session_test.go +++ b/csrf_session_test.go @@ -18,7 +18,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) { // Test that CSRF tokens persist through the authentication flow t.Run("CSRF_Token_Persists_After_Selective_Clear", func(t *testing.T) { // Create a session manager - sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug")) + sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", NewLogger("debug")) require.NoError(t, err) // Create initial request @@ -90,7 +90,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) { // Test that marking session as dirty forces save t.Run("Mark_Dirty_Forces_Session_Save", func(t *testing.T) { - sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug")) + sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", NewLogger("debug")) require.NoError(t, err) req := httptest.NewRequest("GET", "http://example.com/test", nil) @@ -126,7 +126,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) { // Test Azure-specific session handling t.Run("Azure_Session_Cookie_Configuration", func(t *testing.T) { - sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug")) + sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", NewLogger("debug")) require.NoError(t, err) // Simulate Azure callback scenario @@ -158,7 +158,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) { // Test session continuity through auth flow t.Run("Session_Continuity_Through_Auth_Flow", func(t *testing.T) { - sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug")) + sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", NewLogger("debug")) require.NoError(t, err) // Step 1: Initial request @@ -199,7 +199,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) { // Test large token handling doesn't affect CSRF t.Run("Large_Tokens_Dont_Affect_CSRF", func(t *testing.T) { - sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug")) + sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", NewLogger("debug")) require.NoError(t, err) req := httptest.NewRequest("GET", "http://example.com/test", nil) @@ -262,7 +262,7 @@ func TestAuthFlowWithoutExternalDependencies(t *testing.T) { // We can't fully initialize TraefikOidc without network access, // but we can test the session management directly - sessionManager, err := NewSessionManager(plugin.SessionEncryptionKey, plugin.ForceHTTPS, "", NewLogger(plugin.LogLevel)) + sessionManager, err := NewSessionManager(plugin.SessionEncryptionKey, plugin.ForceHTTPS, "", "", NewLogger(plugin.LogLevel)) require.NoError(t, err) t.Run("Session_Created_On_Protected_Request", func(t *testing.T) { @@ -291,7 +291,7 @@ func TestAuthFlowWithoutExternalDependencies(t *testing.T) { // TestRegressionLoginLoop specifically tests the fix for issue #53 func TestRegressionLoginLoop(t *testing.T) { // This test verifies that the specific changes made to fix the login loop work correctly - sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug")) + sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", NewLogger("debug")) require.NoError(t, err) // Simulate the exact flow that was causing the login loop @@ -392,7 +392,7 @@ func TestRegressionLoginLoop(t *testing.T) { // TestCSRFValidationTiming tests timing-sensitive CSRF validation scenarios func TestCSRFValidationTiming(t *testing.T) { - sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug")) + sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", NewLogger("debug")) require.NoError(t, err) t.Run("Rapid_Redirect_Maintains_CSRF", func(t *testing.T) { diff --git a/main.go b/main.go index 406be94..ac2d862 100644 --- a/main.go +++ b/main.go @@ -226,7 +226,7 @@ func NewWithContext(ctx context.Context, config *Config, next http.Handler, name t.logger.Debugf("No custom audience specified, using clientID as audience: %s", t.clientID) } - t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, config.CookieDomain, t.logger) // Safe to ignore: session manager creation with fallback to defaults + t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, config.CookieDomain, config.CookiePrefix, t.logger) // Safe to ignore: session manager creation with fallback to defaults t.errorRecoveryManager = NewErrorRecoveryManager(t.logger) // Initialize token resilience manager with default configuration diff --git a/main_servehttp_test.go b/main_servehttp_test.go index 4a0c69f..1bf54a9 100644 --- a/main_servehttp_test.go +++ b/main_servehttp_test.go @@ -539,7 +539,7 @@ func (m *MockSessionData) Clear(r *http.Request, w http.ResponseWriter) error { // Helper function to create a test session manager func createTestSessionManager(t *testing.T) *SessionManager { - sm, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug")) + sm, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", NewLogger("debug")) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } diff --git a/main_test.go b/main_test.go index ddf932f..8828f84 100644 --- a/main_test.go +++ b/main_test.go @@ -104,7 +104,7 @@ func (ts *TestSuite) Setup() { } logger := NewLogger("info") - ts.sessionManager, _ = NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger) + ts.sessionManager, _ = NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", "", logger) // Create WaitGroup for the OIDC instance goroutineWG := &sync.WaitGroup{} @@ -1274,7 +1274,7 @@ func TestHandleCallback(t *testing.T) { ts.tOidc.tokenBlacklist = NewCache() // Use generic cache for blacklist logger := NewLogger("info") - sessionManager, _ := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger) + sessionManager, _ := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", "", logger) // Create a new instance for each test to avoid state carryover instanceExtractClaimsFunc := tc.extractClaimsFunc @@ -1663,7 +1663,7 @@ func TestHandleLogout(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { logger := NewLogger("info") - sessionManager, _ := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger) + sessionManager, _ := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", "", logger) tOidc := &TraefikOidc{ revocationURL: mockRevocationServer.URL, endSessionURL: tc.endSessionURL, @@ -1966,7 +1966,7 @@ func TestHandleExpiredToken(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { logger := NewLogger("info") - sessionManager, _ := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger) + sessionManager, _ := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", "", logger) tOidc := &TraefikOidc{ sessionManager: sessionManager, diff --git a/memory_leak_consolidated_test.go b/memory_leak_consolidated_test.go index dcf95f7..2141249 100644 --- a/memory_leak_consolidated_test.go +++ b/memory_leak_consolidated_test.go @@ -253,6 +253,7 @@ func TestMemoryLeakConsolidated(t *testing.T) { "test-encryption-key-32-bytes-long-enough", false, "", + "", tf.logger, ) if err != nil { @@ -293,6 +294,7 @@ func TestMemoryLeakConsolidated(t *testing.T) { "test-encryption-key-32-bytes-long-enough", false, "", + "", tf.logger, ) return err @@ -695,6 +697,7 @@ func BenchmarkMemoryUsage(b *testing.B) { "test-encryption-key-32-bytes-long-enough", false, "", + "", NewLogger("error"), ) // No Cleanup method, defer not needed @@ -774,6 +777,7 @@ func TestGoroutineLeaks(t *testing.T) { "test-encryption-key-32-bytes-long-enough", false, "", + "", NewLogger("error"), ) require.NoError(t, err) @@ -863,6 +867,7 @@ func TestMemoryThresholds(t *testing.T) { "test-encryption-key-32-bytes-long-enough", false, "", + "", NewLogger("error"), ) diff --git a/profiling_test.go b/profiling_test.go index 640b265..b915cf8 100644 --- a/profiling_test.go +++ b/profiling_test.go @@ -65,7 +65,7 @@ func TestMemoryTestOrchestrator(t *testing.T) { mto := NewMemoryTestOrchestrator(config, logger) // Test registering a component - sessionManager, err := NewSessionManager("test-key-32-chars-long-for-testing", false, "", logger) + sessionManager, err := NewSessionManager("test-key-32-chars-long-for-testing", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -111,7 +111,7 @@ func TestComponentProfilers(t *testing.T) { logger := NewLogger("debug") // Test Session Pool Profiler - sessionManager, err := NewSessionManager("test-key-32-chars-long-for-testing", false, "", logger) + sessionManager, err := NewSessionManager("test-key-32-chars-long-for-testing", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } diff --git a/regression/regression_test.go b/regression/regression_test.go index 1bb1c0f..1b77e5b 100644 --- a/regression/regression_test.go +++ b/regression/regression_test.go @@ -30,7 +30,7 @@ func testIssue53CSRFRegression(t *testing.T) { // 3. Session cookies must be properly configured for HTTPS // 4. CSRF token must persist through the OAuth flow - sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", traefikoidc.NewLogger("debug")) + sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", "", traefikoidc.NewLogger("debug")) require.NoError(t, err) // Step 1: Initial request to protected resource @@ -116,7 +116,7 @@ func testIssue53CSRFRegression(t *testing.T) { // testIssue53ReverseProxyHTTPS tests HTTPS detection in reverse proxy setups func testIssue53ReverseProxyHTTPS(t *testing.T) { - sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", traefikoidc.NewLogger("debug")) + sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", "", traefikoidc.NewLogger("debug")) require.NoError(t, err) // Create authenticated session with Azure tokens @@ -200,7 +200,7 @@ func testIssue53SameSiteCookies(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", traefikoidc.NewLogger("debug")) + sessionManager, err := traefikoidc.NewSessionManager("test-encryption-key-32-characters", false, "", "", traefikoidc.NewLogger("debug")) require.NoError(t, err) req := httptest.NewRequest("GET", "http://internal/test", nil) diff --git a/security_edge_cases_test.go b/security_edge_cases_test.go index b6a878d..2b1a55f 100644 --- a/security_edge_cases_test.go +++ b/security_edge_cases_test.go @@ -468,7 +468,7 @@ func TestSessionFixationAttack(t *testing.T) { tc := newTestCleanup(t) logger := NewLogger("debug") - sm, err := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger) + sm, err := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -625,7 +625,7 @@ func TestSessionFixationAttack(t *testing.T) { // TestCSRFProtection tests CSRF protection in POST requests func TestCSRFProtection(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger) + sm, err := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } diff --git a/session.go b/session.go index 82b9037..987601d 100644 --- a/session.go +++ b/session.go @@ -237,6 +237,7 @@ type SessionManager struct { logger *Logger chunkManager *ChunkManager cookieDomain string + cookiePrefix string // Prefix for cookie names (default: "_oidc_raczylo_") cleanupMutex sync.RWMutex forceHTTPS bool cleanupDone bool @@ -256,22 +257,29 @@ type SessionManager struct { // - encryptionKey: The key for encrypting session cookies (minimum 32 bytes). // - forceHTTPS: Whether to force HTTPS-only cookies regardless of request scheme. // - cookieDomain: The domain for session cookies (empty for auto-detection). +// - cookiePrefix: Prefix for session cookie names (empty for default "_oidc_raczylo_"). // - logger: Logger instance for debug and error logging. // // Returns: // - The configured SessionManager instance. // - An error if the encryption key does not meet minimum length requirements. -func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain string, logger *Logger) (*SessionManager, error) { +func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain string, cookiePrefix string, logger *Logger) (*SessionManager, error) { if len(encryptionKey) < minEncryptionKeyLength { return nil, fmt.Errorf("encryption key must be at least %d bytes long", minEncryptionKeyLength) } + // Set default cookie prefix if not provided + if cookiePrefix == "" { + cookiePrefix = "_oidc_raczylo_" + } + ctx, cancel := context.WithCancel(context.Background()) sm := &SessionManager{ store: sessions.NewCookieStore([]byte(encryptionKey)), forceHTTPS: forceHTTPS, cookieDomain: cookieDomain, + cookiePrefix: cookiePrefix, logger: logger, chunkManager: NewChunkManager(logger), ctx: ctx, diff --git a/session/core/cookie_prefix_test.go b/session/core/cookie_prefix_test.go new file mode 100644 index 0000000..b751e30 --- /dev/null +++ b/session/core/cookie_prefix_test.go @@ -0,0 +1,127 @@ +package core + +import ( + "testing" +) + +// TestCookiePrefix tests that custom cookie prefixes work correctly +func TestCookiePrefix(t *testing.T) { + tests := []struct { + name string + cookiePrefix string + wantMain string + wantAccess string + wantRefresh string + wantID string + }{ + { + name: "Default prefix", + cookiePrefix: "", + wantMain: "_oidc_raczylo_m", + wantAccess: "_oidc_raczylo_a", + wantRefresh: "_oidc_raczylo_r", + wantID: "_oidc_raczylo_id", + }, + { + name: "Custom prefix", + cookiePrefix: "_oidc_myapp_", + wantMain: "_oidc_myapp_m", + wantAccess: "_oidc_myapp_a", + wantRefresh: "_oidc_myapp_r", + wantID: "_oidc_myapp_id", + }, + { + name: "Custom prefix without underscore suffix", + cookiePrefix: "myapp", + wantMain: "myappm", + wantAccess: "myappa", + wantRefresh: "myappr", + wantID: "myappid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := &MockLogger{} + chunkManager := &MockChunkManager{} + + sm, err := NewSessionManager( + "0123456789abcdef0123456789abcdef0123456789abcdef", + false, + "", + tt.cookiePrefix, + logger, + chunkManager, + ) + if err != nil { + t.Fatalf("Failed to create session manager: %v", err) + } + + // Test cookie names + if got := sm.MainCookieName(); got != tt.wantMain { + t.Errorf("MainCookieName() = %q, want %q", got, tt.wantMain) + } + if got := sm.AccessTokenCookie(); got != tt.wantAccess { + t.Errorf("AccessTokenCookie() = %q, want %q", got, tt.wantAccess) + } + if got := sm.RefreshTokenCookie(); got != tt.wantRefresh { + t.Errorf("RefreshTokenCookie() = %q, want %q", got, tt.wantRefresh) + } + if got := sm.IDTokenCookie(); got != tt.wantID { + t.Errorf("IDTokenCookie() = %q, want %q", got, tt.wantID) + } + }) + } +} + +// TestMultipleInstancesWithDifferentPrefixes tests that multiple session managers +// with different prefixes can coexist (addresses issue #87) +func TestMultipleInstancesWithDifferentPrefixes(t *testing.T) { + logger := &MockLogger{} + chunkManager1 := &MockChunkManager{} + chunkManager2 := &MockChunkManager{} + + // Create two session managers with different prefixes + sm1, err := NewSessionManager( + "0123456789abcdef0123456789abcdef0123456789abcdef", + false, + "example.com", + "_oidc_app1_", + logger, + chunkManager1, + ) + if err != nil { + t.Fatalf("Failed to create session manager 1: %v", err) + } + + sm2, err := NewSessionManager( + "fedcba9876543210fedcba9876543210fedcba9876543210", // Different encryption key + false, + "example.com", + "_oidc_app2_", + logger, + chunkManager2, + ) + if err != nil { + t.Fatalf("Failed to create session manager 2: %v", err) + } + + // Verify they have different cookie names + if sm1.MainCookieName() == sm2.MainCookieName() { + t.Error("Expected different main cookie names for different instances") + } + + // Verify cookie name patterns + expectedPrefix1 := "_oidc_app1_" + expectedPrefix2 := "_oidc_app2_" + + if sm1.MainCookieName() != expectedPrefix1+"m" { + t.Errorf("Expected main cookie name %s, got %s", expectedPrefix1+"m", sm1.MainCookieName()) + } + + if sm2.MainCookieName() != expectedPrefix2+"m" { + t.Errorf("Expected main cookie name %s, got %s", expectedPrefix2+"m", sm2.MainCookieName()) + } + + t.Log("✓ Session isolation verified: Different cookie prefixes prevent session sharing") +} diff --git a/session/core/session_manager.go b/session/core/session_manager.go index 8ea807b..64be200 100644 --- a/session/core/session_manager.go +++ b/session/core/session_manager.go @@ -23,6 +23,7 @@ type SessionManager struct { logger Logger chunkManager ChunkManager cookieDomain string + cookiePrefix string // Prefix for cookie names (default: "_oidc_raczylo_") cleanupMutex sync.RWMutex forceHTTPS bool cleanupDone bool @@ -69,15 +70,21 @@ type SessionData interface { // NewSessionManager creates a new SessionManager instance with secure defaults. // It initializes the cookie store with encryption, sets up session pooling, // and configures chunk management for large tokens. -func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain string, logger Logger, chunkManager ChunkManager) (*SessionManager, error) { +func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain string, cookiePrefix string, logger Logger, chunkManager ChunkManager) (*SessionManager, error) { if len(encryptionKey) < minEncryptionKeyLength { return nil, fmt.Errorf("encryption key must be at least %d bytes long", minEncryptionKeyLength) } + // Set default cookie prefix if not provided + if cookiePrefix == "" { + cookiePrefix = "_oidc_raczylo_" + } + sm := &SessionManager{ store: sessions.NewCookieStore([]byte(encryptionKey)), forceHTTPS: forceHTTPS, cookieDomain: cookieDomain, + cookiePrefix: cookiePrefix, logger: logger, chunkManager: chunkManager, } @@ -114,7 +121,7 @@ func (sm *SessionManager) initializeSession(sessionData SessionData, r *http.Req sessionData.SetManager(sm) // Load session data from cookies - session, err := sm.store.Get(r, MainCookieName()) + session, err := sm.store.Get(r, sm.MainCookieName()) if err != nil { sm.logger.Debugf("Error getting main session: %v", err) return nil // Not a fatal error, will create new session @@ -322,7 +329,14 @@ func (sm *SessionManager) getSessionOptions(isSecure bool) *sessions.Options { } } -// Cookie name functions +// Cookie name methods - these now use the configurable prefix +func (sm *SessionManager) MainCookieName() string { return sm.cookiePrefix + "m" } +func (sm *SessionManager) AccessTokenCookie() string { return sm.cookiePrefix + "a" } +func (sm *SessionManager) RefreshTokenCookie() string { return sm.cookiePrefix + "r" } +func (sm *SessionManager) IDTokenCookie() string { return sm.cookiePrefix + "id" } + +// Package-level functions for backward compatibility (use default prefix) +// These are deprecated and will be removed in a future version func MainCookieName() string { return "_oidc_raczylo_m" } func AccessTokenCookie() string { return "_oidc_raczylo_a" } func RefreshTokenCookie() string { return "_oidc_raczylo_r" } diff --git a/session/core/session_manager_test.go b/session/core/session_manager_test.go index 372b9c5..b4433dc 100644 --- a/session/core/session_manager_test.go +++ b/session/core/session_manager_test.go @@ -165,7 +165,7 @@ func TestSessionManagerCreation(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} - sm, err := NewSessionManager(tt.encryptionKey, false, "", logger, chunkManager) + sm, err := NewSessionManager(tt.encryptionKey, false, "", "", logger, chunkManager) if tt.expectError { if err == nil { @@ -200,7 +200,7 @@ func TestSessionManagerCreation(t *testing.T) { func TestSessionManagerPoolBehavior(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger, chunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -291,7 +291,7 @@ func TestSessionManagerPoolBehavior(t *testing.T) { func TestSessionManagerErrorHandling(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger, chunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -390,7 +390,7 @@ func TestSessionManagerCleanup(t *testing.T) { logger := &MockLogger{} mockChunkManager := &MockChunkManager{} - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, mockChunkManager) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger, mockChunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -458,7 +458,7 @@ func TestSessionManagerHTTPSBehavior(t *testing.T) { chunkManager := &MockChunkManager{} sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", - tt.forceHTTPS, "", logger, chunkManager) + tt.forceHTTPS, "", "", logger, chunkManager) if tt.expectError { if err == nil { @@ -520,7 +520,7 @@ func TestSessionManagerCookieDomain(t *testing.T) { chunkManager := &MockChunkManager{} sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", - false, tt.cookieDomain, logger, chunkManager) + false, tt.cookieDomain, "", logger, chunkManager) if err != nil { t.Errorf("Unexpected error for %s: %v", tt.description, err) @@ -549,7 +549,7 @@ func BenchmarkSessionManagerCreation(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - sm, err := NewSessionManager(encryptionKey, false, "", logger, chunkManager) + sm, err := NewSessionManager(encryptionKey, false, "", "", logger, chunkManager) if err != nil { b.Fatalf("Failed to create session manager: %v", err) } @@ -561,7 +561,7 @@ func BenchmarkSessionManagerCreation(b *testing.B) { func BenchmarkSessionManagerGetSession(b *testing.B) { logger := &MockLogger{} chunkManager := &MockChunkManager{} - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger, chunkManager) if err != nil { b.Fatalf("Failed to create session manager: %v", err) } @@ -599,7 +599,7 @@ func minInt(a, b int) int { func TestValidateSessionHealth(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger, chunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -660,7 +660,7 @@ func TestValidateSessionHealth(t *testing.T) { func TestValidateTokenFormat(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger, chunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -727,7 +727,7 @@ func TestValidateTokenFormat(t *testing.T) { func TestDetectSessionTampering(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger, chunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -812,7 +812,7 @@ func TestGetSessionMetrics(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", - tt.forceHTTPS, tt.cookieDomain, logger, chunkManager) + tt.forceHTTPS, tt.cookieDomain, "", logger, chunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -898,7 +898,7 @@ func TestShouldUseSecureCookies(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", - tt.forceHTTPS, "", logger, chunkManager) + tt.forceHTTPS, "", "", logger, chunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -940,7 +940,7 @@ func TestGetSessionOptions(t *testing.T) { logger := &MockLogger{} chunkManager := &MockChunkManager{} sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", - false, tt.cookieDomain, logger, chunkManager) + false, tt.cookieDomain, "", logger, chunkManager) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } diff --git a/session_helpers_test.go b/session_helpers_test.go index b221137..e1cc345 100644 --- a/session_helpers_test.go +++ b/session_helpers_test.go @@ -11,7 +11,7 @@ import ( // TestSetCodeVerifier_NoChange tests the branch where the code verifier value doesn't change func TestSetCodeVerifier_NoChange(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -52,7 +52,7 @@ func TestSetCodeVerifier_NoChange(t *testing.T) { // TestClearTokenChunks_EmptyChunks tests the branch where the chunks map is empty func TestClearTokenChunks_EmptyChunks(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -90,7 +90,7 @@ func TestClearTokenChunks_EmptyChunks(t *testing.T) { // TestClearTokenChunks_WithSessions tests the branch where the chunks map contains actual sessions func TestClearTokenChunks_WithSessions(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } diff --git a/session_test.go b/session_test.go index 3f28599..e8b42e4 100644 --- a/session_test.go +++ b/session_test.go @@ -84,7 +84,7 @@ func TestSessionPoolMemoryLeak(t *testing.T) { } logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -107,7 +107,7 @@ func TestSessionPoolMemoryLeak(t *testing.T) { session.ReturnToPool() case "Error path in GetSession": - badSM, _ := NewSessionManager("different0123456789abcdef0123456789abcdef0123456789", false, "", logger) + badSM, _ := NewSessionManager("different0123456789abcdef0123456789abcdef0123456789", false, "", "", logger) _, err = badSM.GetSession(req) if err == nil { t.Log("Note: Expected error when using mismatched encryption keys") @@ -172,7 +172,7 @@ func TestSessionErrorHandling(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -226,7 +226,7 @@ func TestSessionClearAlwaysReturnsToPool(t *testing.T) { Timeout: 30 * time.Second, Operation: func() error { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { return fmt.Errorf("failed to create session manager: %w", err) } @@ -264,7 +264,7 @@ func TestSessionClearAlwaysReturnsToPool(t *testing.T) { // Additional verification test t.Run("Verify pool still works after errors", func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -324,7 +324,7 @@ func TestSessionObjectTracking(t *testing.T) { } logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -574,7 +574,7 @@ func TestTokenChunkingIntegrity(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -681,7 +681,7 @@ func TestTokenChunkingCorruptionResistance(t *testing.T) { for _, test := range corruptionTests { t.Run(test.Name, func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -772,7 +772,7 @@ func TestTokenSizeLimits(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -837,7 +837,7 @@ func TestConcurrentTokenOperations(t *testing.T) { Timeout: 60 * time.Second, Operation: func() error { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { return fmt.Errorf("failed to create session manager: %w", err) } @@ -925,7 +925,7 @@ func TestSessionValidationAndCleanup(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -1010,7 +1010,7 @@ func TestLargeIDTokenChunking(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -1115,7 +1115,7 @@ func BenchmarkSessionOperations(b *testing.B) { perfHelper := NewPerformanceTestHelper() logger := NewLogger("error") // Reduce logging for benchmarks - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { b.Fatalf("Failed to create session manager: %v", err) } @@ -1256,7 +1256,7 @@ func TestSessionStatePreservationWithExpiredTokens(t *testing.T) { t.Log("Testing session state preservation with expired tokens - this test demonstrates BROKEN BEHAVIOR") logger := NewLogger("debug") - sm, err := NewSessionManager("test-session-key-32-bytes-long-12345", false, "", logger) + sm, err := NewSessionManager("test-session-key-32-bytes-long-12345", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -1452,7 +1452,7 @@ func TestSessionExpiryVsTokenExpiry(t *testing.T) { t.Log("Testing session expiry vs token expiry distinction - validating proper session and token lifetime management") logger := NewLogger("debug") - sm, err := NewSessionManager("session-vs-token-test-key-32-bytes", false, "", logger) + sm, err := NewSessionManager("session-vs-token-test-key-32-bytes", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -1591,7 +1591,7 @@ func TestSessionCleanupOnTokenExpiry(t *testing.T) { t.Log("Testing session cleanup on token expiry - validating proper session data management") logger := NewLogger("debug") - sm, err := NewSessionManager("cleanup-test-key-32-bytes-long-123", false, "", logger) + sm, err := NewSessionManager("cleanup-test-key-32-bytes-long-123", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } diff --git a/settings.go b/settings.go index 2fd1b19..04b8400 100644 --- a/settings.go +++ b/settings.go @@ -30,6 +30,7 @@ type Config struct { HTTPClient *http.Client `json:"-"` OIDCEndSessionURL string `json:"oidcEndSessionURL"` CookieDomain string `json:"cookieDomain"` + CookiePrefix string `json:"cookiePrefix"` // Prefix for session cookie names (default: "_oidc_raczylo_") CallbackURL string `json:"callbackURL"` LogoutURL string `json:"logoutURL"` ClientID string `json:"clientID"` diff --git a/test_framework_test.go b/test_framework_test.go index fd06116..344bb02 100644 --- a/test_framework_test.go +++ b/test_framework_test.go @@ -279,6 +279,7 @@ func (tf *TestFramework) CreateAuthenticatedRequest(method, path string) (*http. tf.fixtures.EncryptionKey, false, "", + "", tf.oidc.logger, ) if err != nil { @@ -323,6 +324,7 @@ func (tf *TestFramework) CreateCallbackRequest() *http.Request { tf.fixtures.EncryptionKey, false, "", + "", tf.oidc.logger, ) diff --git a/test_helpers_adapter_test.go b/test_helpers_adapter_test.go index 6a842ff..e911c7a 100644 --- a/test_helpers_adapter_test.go +++ b/test_helpers_adapter_test.go @@ -204,7 +204,7 @@ func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httpt logInfo: log.New(&testWriter{t}, "INFO: ", 0), logDebug: log.New(&testWriter{t}, "DEBUG: ", 0), } - sessionManager, _ := NewSessionManager(config.SessionEncryptionKey, false, "", logger) + sessionManager, _ := NewSessionManager(config.SessionEncryptionKey, false, "", "", logger) // Create next handler nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -350,7 +350,7 @@ func createMockJWT(t *testing.T, sub, email string) string { func createTestSession() *SessionData { // Create a minimal session manager for testing logger := newNoOpLogger() - sessionManager, _ := NewSessionManager("test-encryption-key-32-characters", false, "", logger) + sessionManager, _ := NewSessionManager("test-encryption-key-32-characters", false, "", "", logger) // Create a test request req := httptest.NewRequest("GET", "/", nil) diff --git a/token_consolidated_test.go b/token_consolidated_test.go index 49644a3..25a6243 100644 --- a/token_consolidated_test.go +++ b/token_consolidated_test.go @@ -164,7 +164,7 @@ func TestTokenTypes(t *testing.T) { func TestTokenCorruption(t *testing.T) { t.Run("TokenCorruptionScenario", func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -291,7 +291,7 @@ func TestTokenCorruption(t *testing.T) { func TestTokenResilience(t *testing.T) { t.Run("ConcurrentTokenAccess", func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) } @@ -337,7 +337,7 @@ func TestTokenResilience(t *testing.T) { t.Run("TokenSizeHandling", func(t *testing.T) { logger := NewLogger("debug") - sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger) + sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", logger) if err != nil { t.Fatalf("Failed to create session manager: %v", err) }