CSRF and Cookie Domain fixes

This commit is contained in:
2025-08-28 10:45:19 +01:00
parent e2e2be53c1
commit 5241cfe792
22 changed files with 649 additions and 70 deletions
+26
View File
@@ -77,6 +77,7 @@ testData:
revocationURL: https://accounts.google.com/revoke # Endpoint for revoking tokens
oidcEndSessionURL: https://accounts.google.com/logout # Provider's end session endpoint
enablePKCE: false # Enables PKCE (Proof Key for Code Exchange) for additional security
cookieDomain: "" # Explicit domain for session cookies (e.g., ".example.com" for multi-subdomain setups)
# --- Provider Specific Configuration Examples ---
#
@@ -373,6 +374,31 @@ configuration:
Default: false
required: false
cookieDomain:
type: string
description: |
Explicit domain for session cookies. This is important for multi-subdomain setups
and reverse proxy deployments to ensure consistent cookie handling.
When set, all session cookies will use this domain. When not set, the domain
is auto-detected from the request headers (X-Forwarded-Host or Host).
Use a leading dot for subdomain-wide cookies (e.g., ".example.com" allows
cookies to be shared between app.example.com, api.example.com, etc.).
Use a specific domain for host-only cookies (e.g., "app.example.com" restricts
cookies to that exact domain).
This setting is crucial to prevent authentication issues like "CSRF token missing
in session" errors that can occur when cookies are created with inconsistent domains.
Examples:
- ".example.com" - Allows all subdomains to share cookies
- "app.example.com" - Restricts cookies to this specific host
Default: "" (auto-detected from request headers)
required: false
headers:
type: array
description: |
+25
View File
@@ -82,6 +82,7 @@ The middleware supports the following configuration options:
| `oidcEndSessionURL` | The provider's end session endpoint | auto-discovered | `https://accounts.google.com/logout` |
| `enablePKCE` | Enables PKCE (Proof Key for Code Exchange) for authorization code flow | `false` | `true`, `false` |
| `refreshGracePeriodSeconds` | Seconds before token expiry to attempt proactive refresh | `60` | `120` |
| `cookieDomain` | Explicit domain for session cookies (important for multi-subdomain setups) | auto-detected | `.example.com`, `app.example.com` |
| `headers` | Custom HTTP headers with templates that can access OIDC claims and tokens | none | See "Templated Headers" section |
## Scope Configuration
@@ -311,6 +312,30 @@ spec:
- developer
```
### With Cookie Domain Configuration (Multi-Subdomain Setup)
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-multi-subdomain
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
cookieDomain: .example.com # Allows cookies to be shared across all subdomains
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
```
**Important**: The `cookieDomain` parameter is crucial when running behind a reverse proxy or when your application serves multiple subdomains. Without it, cookies may be created with inconsistent domains, leading to authentication issues like "CSRF token missing in session" errors.
### With Custom Logging and Rate Limiting
```yaml
+3 -3
View File
@@ -14,7 +14,7 @@ import (
// during the callback from Azure AD
func TestAzureOAuthCallbackScenario(t *testing.T) {
t.Run("Azure_OAuth_Complete_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: User visits https://app.example.com/protected
@@ -110,7 +110,7 @@ func TestAzureOAuthCallbackScenario(t *testing.T) {
t.Run("Cookie_Not_Sent_With_Wrong_Domain", func(t *testing.T) {
// This test verifies that cookies with wrong domain won't be sent
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)
// Initial request sets cookie for app.example.com
@@ -148,7 +148,7 @@ func TestAzureOAuthCallbackScenario(t *testing.T) {
// This test demonstrates why we can't use SameSite=Strict for OAuth
// With Strict, cookies wouldn't be sent when redirecting from Azure to our app
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)
// If we had SameSite=Strict (which we don't anymore), the browser would:
+1 -1
View File
@@ -71,7 +71,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
+218
View File
@@ -0,0 +1,218 @@
package traefikoidc
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// TestCookieCleanupBehavior tests the cookie cleanup function's behavior
func TestCookieCleanupBehavior(t *testing.T) {
logger := NewLogger("debug")
// Create session manager with a specific domain
sm, err := NewSessionManager("test-encryption-key-32-characters", false, "app.example.com", logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
// Create request from app.example.com
req := httptest.NewRequest("GET", "http://app.example.com/test", nil)
// Add some OIDC cookies (browsers don't send domain info)
cookies := []*http.Cookie{
{
Name: "_oidc_raczylo_m",
Value: "test-value-1",
},
{
Name: "_oidc_raczylo_a",
Value: "test-value-2",
},
{
Name: "access_token_chunk_0",
Value: "chunk-value",
},
}
for _, cookie := range cookies {
req.AddCookie(cookie)
}
// Create response recorder to capture Set-Cookie headers
rr := httptest.NewRecorder()
// Run cleanup - it should attempt to delete cookies with various domains
sm.CleanupOldCookies(rr, req)
// Check Set-Cookie headers
setCookies := rr.Result().Cookies()
// The cleanup should have attempted to delete cookies with various domain variations
// We should see deletion attempts for:
// - app.example.com
// - .app.example.com
// - example.com
// - .example.com
var deletionAttempts int
domainsAttempted := make(map[string]bool)
for _, cookie := range setCookies {
if cookie.MaxAge == -1 {
deletionAttempts++
domainsAttempted[cookie.Domain] = true
}
}
// We should see deletion attempts for various domain variations
// Note: The exact number depends on the implementation, but we should see multiple attempts
if deletionAttempts == 0 {
t.Error("Expected cleanup to attempt cookie deletions, but none were found")
}
// Log the domains attempted for debugging
t.Logf("Deletion attempts: %d", deletionAttempts)
for domain := range domainsAttempted {
t.Logf("Attempted deletion for domain: %q", domain)
}
}
// TestConfiguredDomainPersistence tests that configured domain is consistently used
func TestConfiguredDomainPersistence(t *testing.T) {
logger := NewLogger("debug")
// Create session manager with explicit domain configuration
configuredDomain := ".example.com"
sm, err := NewSessionManager("test-encryption-key-32-characters", false, configuredDomain, logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
// Create requests from different subdomains
tests := []struct {
requestHost string
name string
}{
{
name: "Request from main domain",
requestHost: "example.com",
},
{
name: "Request from subdomain",
requestHost: "app.example.com",
},
{
name: "Request from nested subdomain",
requestHost: "api.app.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "http://"+tt.requestHost+"/test", nil)
rr := httptest.NewRecorder()
// Get session and set a value
session, err := sm.GetSession(req)
if err != nil {
t.Fatalf("Failed to get session: %v", err)
}
defer session.ReturnToPool()
session.SetNonce("test-nonce")
// Save session
err = session.Save(req, rr)
if err != nil {
t.Fatalf("Failed to save session: %v", err)
}
// Check that all cookies use the configured domain
cookies := rr.Result().Cookies()
for _, cookie := range cookies {
if strings.HasPrefix(cookie.Name, "_oidc_") {
// The domain should match the configured domain (with possible normalization)
expectedDomains := []string{configuredDomain, strings.TrimPrefix(configuredDomain, ".")}
domainMatches := false
for _, expected := range expectedDomains {
if cookie.Domain == expected || cookie.Domain == "" {
domainMatches = true
break
}
}
if !domainMatches {
t.Errorf("Cookie %s has unexpected domain %q, expected one of %v",
cookie.Name, cookie.Domain, expectedDomains)
}
}
}
})
}
}
// TestDomainMigration simulates migrating from no configured domain to explicit domain
func TestDomainMigration(t *testing.T) {
logger := NewLogger("debug")
// Step 1: Create session without configured domain (auto-detection)
sm1, err := NewSessionManager("test-encryption-key-32-characters", false, "", logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
req1 := httptest.NewRequest("GET", "http://app.example.com/test", nil)
rr1 := httptest.NewRecorder()
session1, err := sm1.GetSession(req1)
if err != nil {
t.Fatalf("Failed to get session: %v", err)
}
session1.SetNonce("test-nonce")
session1.Save(req1, rr1)
session1.ReturnToPool()
// The cookies will have auto-detected domain
oldCookies := rr1.Result().Cookies()
t.Logf("Old cookies count: %d", len(oldCookies))
// Step 2: Create new session manager with explicit domain configuration
sm2, err := NewSessionManager("test-encryption-key-32-characters", false, ".example.com", logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
// Step 3: Make request with old cookies
req2 := httptest.NewRequest("GET", "http://app.example.com/test", nil)
// Add the old cookies to the new request
for _, cookie := range oldCookies {
// Simulate browser behavior - don't include domain in Cookie header
simpleCookie := &http.Cookie{
Name: cookie.Name,
Value: cookie.Value,
}
req2.AddCookie(simpleCookie)
}
rr2 := httptest.NewRecorder()
// Run cleanup - should attempt to delete old cookies
sm2.CleanupOldCookies(rr2, req2)
// Check that deletion cookies were sent
newCookies := rr2.Result().Cookies()
deletionCount := 0
for _, cookie := range newCookies {
if cookie.MaxAge == -1 {
deletionCount++
}
}
if deletionCount == 0 {
t.Error("Expected cleanup to send deletion cookies during migration, but none were found")
}
t.Logf("Sent %d deletion cookies during migration", deletionCount)
}
+205
View File
@@ -0,0 +1,205 @@
package traefikoidc
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// TestCookieDomainConfiguration tests that the cookie domain configuration is properly applied
func TestCookieDomainConfiguration(t *testing.T) {
tests := []struct {
configDomain string
requestHost string
forwardedHost string
expectedDomain string
name string
}{
{
name: "Configured domain takes precedence",
configDomain: ".example.com",
requestHost: "app.example.com",
expectedDomain: ".example.com",
},
{
name: "Auto-detection when no domain configured",
configDomain: "",
requestHost: "app.example.com",
expectedDomain: "app.example.com",
},
{
name: "X-Forwarded-Host used for auto-detection",
configDomain: "",
requestHost: "internal.local",
forwardedHost: "public.example.com",
expectedDomain: "public.example.com",
},
{
name: "No domain for localhost",
configDomain: "",
requestHost: "localhost:8080",
expectedDomain: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create session manager with configured domain
logger := NewLogger("debug")
sm, err := NewSessionManager("test-encryption-key-32-characters", false, tt.configDomain, logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
// Create request
req := httptest.NewRequest("GET", "http://"+tt.requestHost+"/test", nil)
if tt.forwardedHost != "" {
req.Header.Set("X-Forwarded-Host", tt.forwardedHost)
}
// Create a dummy response writer to test getCookieOptions behavior
// We'll examine the session options domain instead
options := &http.Cookie{
Domain: sm.cookieDomain,
}
// If no configured domain, simulate auto-detection
if sm.cookieDomain == "" && req != nil {
host := req.Host
if forwardedHost := req.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
}
if host != "" && !strings.Contains(host, "localhost") && !strings.Contains(host, "127.0.0.1") {
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex]
}
options.Domain = host
}
}
// Check domain
if options.Domain != tt.expectedDomain {
t.Errorf("Expected domain %q, got %q", tt.expectedDomain, options.Domain)
}
})
}
}
// TestCookieDomainConsistency tests that all session cookies use the same domain
func TestCookieDomainConsistency(t *testing.T) {
logger := NewLogger("debug")
// Test with configured domain
sm, err := NewSessionManager("test-encryption-key-32-characters", false, ".example.com", logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
req := httptest.NewRequest("GET", "http://app.example.com/test", nil)
rr := httptest.NewRecorder()
// Get session and set some values
session, err := sm.GetSession(req)
if err != nil {
t.Fatalf("Failed to get session: %v", err)
}
defer session.ReturnToPool()
// Set various session values including ID token
session.SetNonce("test-nonce")
session.SetAccessToken("test-access-token")
session.SetRefreshToken("test-refresh-token")
// Set a valid JWT-like ID token (needs 2 dots)
session.SetIDToken("header.payload.signature")
// Save session
err = session.Save(req, rr)
if err != nil {
t.Fatalf("Failed to save session: %v", err)
}
// Check all cookies have the same domain
cookies := rr.Result().Cookies()
var seenDomain string
for _, cookie := range cookies {
// Only check our OIDC cookies
if strings.HasPrefix(cookie.Name, "_oidc_") ||
strings.HasPrefix(cookie.Name, "access_token_chunk_") ||
strings.HasPrefix(cookie.Name, "refresh_token_chunk_") {
// Normalize domain for comparison (handle leading dot and empty domain)
normalizedDomain := cookie.Domain
if normalizedDomain == "" {
// Empty domain means host-only cookie, should match configured domain
normalizedDomain = "example.com"
} else if strings.HasPrefix(normalizedDomain, ".") {
normalizedDomain = normalizedDomain[1:]
}
if seenDomain == "" {
seenDomain = normalizedDomain
} else if normalizedDomain != seenDomain {
t.Errorf("Inconsistent cookie domains: %q vs %q for cookie %s",
seenDomain, normalizedDomain, cookie.Name)
}
// Verify it matches configured domain (browsers may normalize by removing leading dot)
// Empty domain is also acceptable for host-only cookies
if cookie.Domain != "" && cookie.Domain != ".example.com" && cookie.Domain != "example.com" {
t.Errorf("Cookie %s has domain %q, expected %q, %q, or empty",
cookie.Name, cookie.Domain, ".example.com", "example.com")
}
}
}
}
// TestCookieDomainWithReverseProxy simulates a reverse proxy scenario
func TestCookieDomainWithReverseProxy(t *testing.T) {
logger := NewLogger("debug")
// No configured domain, should auto-detect from X-Forwarded-Host
sm, err := NewSessionManager("test-encryption-key-32-characters", false, "", logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
// Simulate reverse proxy request
req := httptest.NewRequest("GET", "http://internal.local:8080/test", nil)
req.Header.Set("X-Forwarded-Host", "public.example.com")
req.Header.Set("X-Forwarded-Proto", "https")
// Test the domain configuration
// Since getCookieOptions is private, we'll check the configured domain directly
options := &http.Cookie{
Domain: sm.cookieDomain,
}
// If no configured domain, simulate auto-detection
if sm.cookieDomain == "" && req != nil {
host := req.Host
if forwardedHost := req.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
}
if host != "" && !strings.Contains(host, "localhost") && !strings.Contains(host, "127.0.0.1") {
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex]
}
options.Domain = host
}
}
// Check secure flag based on X-Forwarded-Proto
isSecure := req.Header.Get("X-Forwarded-Proto") == "https" || req.TLS != nil
options.Secure = isSecure // Note: forceHTTPS is private so we can't access it in test
// Should use the forwarded host
if options.Domain != "public.example.com" {
t.Errorf("Expected domain from X-Forwarded-Host %q, got %q",
"public.example.com", options.Domain)
}
// Should be secure due to X-Forwarded-Proto
if !options.Secure {
t.Error("Expected Secure flag to be true with X-Forwarded-Proto: https")
}
}
+8 -8
View File
@@ -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) {
+2 -2
View File
@@ -43,7 +43,7 @@ func TestGoogleOIDCRefreshTokenHandling(t *testing.T) {
}
// Create a session manager
sessionManager, _ := NewSessionManager("0123456789abcdef0123456789abcdef", true, mockLogger)
sessionManager, _ := NewSessionManager("0123456789abcdef0123456789abcdef", true, "", mockLogger)
tOidc.sessionManager = sessionManager
t.Run("Google provider detection adds required parameters", func(t *testing.T) {
@@ -367,7 +367,7 @@ func TestGoogleOIDCRefreshTokenHandling(t *testing.T) {
}
// Create a session manager
sessionManager, _ := NewSessionManager("0123456789abcdef0123456789abcdef", true, mockLogger)
sessionManager, _ := NewSessionManager("0123456789abcdef0123456789abcdef", true, "", mockLogger)
googleTOidc.sessionManager = sessionManager
// Create a mock token verifier
+5 -5
View File
@@ -20,7 +20,7 @@ func TestIssue53Regression(t *testing.T) {
// 3. Session cookies must be properly configured for HTTPS
// 4. CSRF token must persist through the OAuth flow
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 to protected resource
@@ -108,7 +108,7 @@ func TestIssue53Regression(t *testing.T) {
// This test ensures that once the session is properly maintained,
// token signature verification works correctly for Azure tokens
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 authenticated session with Azure tokens
@@ -154,7 +154,7 @@ func TestIssue53Regression(t *testing.T) {
// This test verifies the redirect loop prevention mechanism
// that was added to handle authentication failures gracefully
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://internal/protected", nil)
@@ -183,7 +183,7 @@ func TestIssue53Regression(t *testing.T) {
// in different reverse proxy scenarios
func TestReverseProxySameSiteHandling(t *testing.T) {
t.Run("SameSite_Lax_For_HTTPS_OAuth", 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)
// HTTPS request via proxy
@@ -211,7 +211,7 @@ func TestReverseProxySameSiteHandling(t *testing.T) {
})
t.Run("SameSite_Lax_For_HTTP", 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)
// Plain HTTP request (no proxy headers)
+5 -1
View File
@@ -756,7 +756,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
suppressDiagnosticLogs: isTestMode(), // Suppress diagnostic logs during tests
}
t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, t.logger)
t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, config.CookieDomain, t.logger)
t.errorRecoveryManager = NewErrorRecoveryManager(t.logger)
t.extractClaimsFunc = extractClaims
// t.exchangeCodeForTokenFunc = t.exchangeCodeForToken // Removed, using interface now
@@ -1087,6 +1087,10 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
// Clean up any old cookies with different domains before processing
// This prevents CSRF token mismatch errors when cookie domain configuration changes
t.sessionManager.CleanupOldCookies(rw, req)
// Retrieve or create session for request
session, err := t.sessionManager.GetSession(req)
if err != nil {
+4 -4
View File
@@ -90,7 +90,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)
// Common TraefikOidc instance
ts.tOidc = &TraefikOidc{
@@ -1173,7 +1173,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
@@ -1561,7 +1561,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,
@@ -1863,7 +1863,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,
+1 -1
View File
@@ -72,7 +72,7 @@ func TestMemoryLeakFixes(t *testing.T) {
t.Run("Session pool returns properly", func(t *testing.T) {
logger := NewLogger("debug")
sm, err := NewSessionManager("test-encryption-key-that-is-long-enough-32bytes", false, logger)
sm, err := NewSessionManager("test-encryption-key-that-is-long-enough-32bytes", false, "", logger)
if err != nil {
t.Fatal(err)
}
+9 -9
View File
@@ -12,7 +12,7 @@ import (
// TestReverseProxyHTTPSDetection tests that HTTPS is properly detected in reverse proxy environments
func TestReverseProxyHTTPSDetection(t *testing.T) {
t.Run("HTTPS_Detection_With_X_Forwarded_Proto", 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 request from reverse proxy (Traefik/nginx)
@@ -48,7 +48,7 @@ func TestReverseProxyHTTPSDetection(t *testing.T) {
})
t.Run("HTTPS_Detection_Without_Headers", 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)
// Request without reverse proxy headers (direct HTTP)
@@ -79,7 +79,7 @@ func TestReverseProxyHTTPSDetection(t *testing.T) {
t.Run("HTTPS_Detection_With_ForceHTTPS", func(t *testing.T) {
// Test with forceHTTPS enabled
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", true, NewLogger("debug"))
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", true, "", NewLogger("debug"))
require.NoError(t, err)
// Even without headers, forceHTTPS should make cookies secure
@@ -110,7 +110,7 @@ func TestReverseProxyHTTPSDetection(t *testing.T) {
// TestCSRFPersistenceInReverseProxy tests CSRF token persistence in reverse proxy setups
func TestCSRFPersistenceInReverseProxy(t *testing.T) {
t.Run("CSRF_Persists_Through_OAuth_Flow_With_Proxy", 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 to protected resource (HTTPS via proxy)
@@ -156,7 +156,7 @@ func TestCSRFPersistenceInReverseProxy(t *testing.T) {
})
t.Run("Session_Cookie_Domain_With_Proxy_Headers", 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)
// Test with X-Forwarded-Host header
@@ -196,7 +196,7 @@ func TestAzureOIDCWithReverseProxy(t *testing.T) {
// This test verifies Azure-specific provider detection and configuration
// without making actual network calls
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: Test session setup for Azure OAuth flow
@@ -254,7 +254,7 @@ func TestAzureOIDCWithReverseProxy(t *testing.T) {
})
t.Run("Mixed_HTTP_HTTPS_Requests", 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 a scenario where some requests come via HTTPS proxy and some don't
@@ -299,7 +299,7 @@ func TestAzureOIDCWithReverseProxy(t *testing.T) {
// TestEnhanceSessionSecurity verifies the security enhancement function
func TestEnhanceSessionSecurity(t *testing.T) {
t.Run("Security_Enhancement_For_AJAX_Requests", 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)
// AJAX request via HTTPS proxy
@@ -327,7 +327,7 @@ func TestEnhanceSessionSecurity(t *testing.T) {
})
t.Run("Security_Enhancement_Missing_User_Agent", 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)
// Request without User-Agent (potential bot/attack)
+1 -1
View File
@@ -197,7 +197,7 @@ func TestCacheMemoryExhaustion(t *testing.T) {
// TestSessionConcurrencyProtection tests session safety under concurrent access
func TestSessionConcurrencyProtection(t *testing.T) {
logger := NewLogger("debug")
sessionManager, err := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, logger)
sessionManager, 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)
}
+2 -2
View File
@@ -461,7 +461,7 @@ func TestMissingClaims(t *testing.T) {
// TestSessionFixationAttack tests the plugin's resistance to session fixation attacks
func TestSessionFixationAttack(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)
}
@@ -615,7 +615,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)
}
+106 -6
View File
@@ -243,17 +243,20 @@ type SessionManager struct {
store sessions.Store
logger *Logger
forceHTTPS bool
cookieDomain string
chunkManager *ChunkManager
cleanupDone bool // Track if we've attempted cookie cleanup
}
// NewSessionManager creates a new session manager with the specified configuration.
// Parameters:
// - encryptionKey: Key used to encrypt session data (must be at least 32 bytes)
// - forceHTTPS: When true, forces secure cookie attributes regardless of request scheme
// - cookieDomain: Explicit domain for cookies (e.g., ".example.com" for subdomain access)
// - logger: Logger instance for recording session-related events
//
// Returns an error if the encryption key does not meet minimum length requirements.
func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*SessionManager, error) {
func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain string, logger *Logger) (*SessionManager, error) {
// Validate encryption key length.
if len(encryptionKey) < minEncryptionKeyLength {
return nil, fmt.Errorf("encryption key must be at least %d bytes long", minEncryptionKeyLength)
@@ -262,6 +265,7 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*
sm := &SessionManager{
store: sessions.NewCookieStore([]byte(encryptionKey)),
forceHTTPS: forceHTTPS,
cookieDomain: cookieDomain,
logger: logger,
chunkManager: NewChunkManager(logger),
}
@@ -512,8 +516,13 @@ func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *h
// Always enforce security best practices
options.HttpOnly = true
// Set secure Domain attribute for production
if options.Domain == "" && r != nil {
// Set cookie domain from configuration if specified
if sm.cookieDomain != "" {
// Use the explicitly configured domain
options.Domain = sm.cookieDomain
sm.logger.Debugf("Using configured cookie domain: %s", sm.cookieDomain)
} else if options.Domain == "" && r != nil {
// Fall back to auto-detection only if no domain is configured
// In reverse proxy setups, use the original host if available
// This ensures cookies work correctly with the public domain
host := r.Host
@@ -529,10 +538,10 @@ func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *h
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex] // Remove port
}
// For cookie domain, we might want to use the base domain
// to allow cookies to work across subdomains
// But for now, use the exact host for security
// Use the exact host (without leading dot) for host-only cookies
// This prevents subdomain access but ensures consistency
options.Domain = host
sm.logger.Debugf("Auto-detected cookie domain: %s", host)
}
}
@@ -555,10 +564,101 @@ func (sm *SessionManager) getSessionOptions(isSecure bool) *sessions.Options {
SameSite: http.SameSiteLaxMode,
MaxAge: int(absoluteSessionTimeout.Seconds()),
Path: "/",
Domain: sm.cookieDomain, // Use configured domain if set
}
return baseOptions
}
// CleanupOldCookies attempts to remove cookies with potentially mismatched domains.
// Since browsers don't send domain info with cookies, this function tries to delete
// cookies with various domain variations to ensure cleanup after configuration changes.
func (sm *SessionManager) CleanupOldCookies(w http.ResponseWriter, r *http.Request) {
// Get all cookies from the request
cookies := r.Cookies()
// Get the current configured domain
currentDomain := sm.cookieDomain
host := r.Host
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
}
// Remove port if present
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex]
}
// Build list of domains to attempt deletion for
// This ensures we clean up cookies from various possible domains
var domainsToClean []string
// Add variations of the current host
if host != "" && !strings.Contains(host, "localhost") && !strings.Contains(host, "127.0.0.1") {
domainsToClean = append(domainsToClean,
host, // exact host
"."+host, // with leading dot
)
// Add parent domain variations if host has subdomains
parts := strings.Split(host, ".")
if len(parts) > 2 {
// For "sub.example.com", also try "example.com" and ".example.com"
parentDomain := strings.Join(parts[len(parts)-2:], ".")
domainsToClean = append(domainsToClean,
parentDomain,
"."+parentDomain,
)
}
}
// Track which cookies we're processing
processedCookies := make(map[string]bool)
// Check each cookie
for _, cookie := range cookies {
// Only process our OIDC session cookies
if strings.HasPrefix(cookie.Name, mainCookieName) ||
strings.HasPrefix(cookie.Name, accessTokenCookie) ||
strings.HasPrefix(cookie.Name, refreshTokenCookie) ||
strings.HasPrefix(cookie.Name, "_oidc_raczylo_id") ||
strings.HasPrefix(cookie.Name, "access_token_chunk_") ||
strings.HasPrefix(cookie.Name, "refresh_token_chunk_") {
// Mark this cookie as seen
processedCookies[cookie.Name] = true
// If we have a configured domain and this is the first request after config change,
// attempt to delete the cookie with various domain variations to ensure cleanup
if currentDomain != "" && !sm.cleanupDone {
for _, domain := range domainsToClean {
// Skip the current configured domain
if domain == currentDomain || domain == "."+currentDomain || "."+domain == currentDomain {
continue
}
// Create a deletion cookie for this domain variation
deleteCookie := &http.Cookie{
Name: cookie.Name,
Value: "",
Path: "/",
Domain: domain,
MaxAge: -1,
HttpOnly: true,
Secure: r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil || sm.forceHTTPS,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, deleteCookie)
sm.logger.Debugf("Attempting to clean up cookie %s with domain %s", cookie.Name, domain)
}
}
}
}
// Mark cleanup as done for this session manager instance
if !sm.cleanupDone && len(processedCookies) > 0 {
sm.cleanupDone = true
}
}
// GetSession retrieves all session data for the current request.
// It loads the main session and token sessions, including any chunked token data,
// and combines them into a single SessionData structure for easy access.
+5 -5
View File
@@ -14,7 +14,7 @@ import (
func TestSessionCompatibilityAfterChanges(t *testing.T) {
t.Run("Plain_HTTP_Without_Proxy_Headers", func(t *testing.T) {
// Test that plain HTTP requests without proxy headers still work
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://localhost:8080/test", nil)
@@ -67,7 +67,7 @@ func TestSessionCompatibilityAfterChanges(t *testing.T) {
t.Run("HTTPS_With_TLS_Field", func(t *testing.T) {
// Test direct HTTPS connection (not proxied)
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", "https://example.com/test", nil)
@@ -101,7 +101,7 @@ func TestSessionCompatibilityAfterChanges(t *testing.T) {
t.Run("ForceHTTPS_Setting", func(t *testing.T) {
// Test forceHTTPS setting works regardless of request
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", true, NewLogger("debug"))
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", true, "", NewLogger("debug"))
require.NoError(t, err)
// Plain HTTP request
@@ -132,7 +132,7 @@ func TestSessionCompatibilityAfterChanges(t *testing.T) {
})
t.Run("AJAX_Request_Gets_Strict_SameSite", 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)
// AJAX request
@@ -165,7 +165,7 @@ func TestSessionCompatibilityAfterChanges(t *testing.T) {
})
t.Run("Missing_UserAgent_Gets_Reduced_Timeout", 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)
// Request without User-Agent (suspicious)
+11 -11
View File
@@ -16,7 +16,7 @@ import (
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)
}
@@ -44,7 +44,7 @@ func TestSessionPoolMemoryLeak(t *testing.T) {
// Test 3: Error path in GetSession
// Modify the session store to force an error - use a different encryption key
badSM, _ := NewSessionManager("different0123456789abcdef0123456789abcdef0123456789", false, logger)
badSM, _ := NewSessionManager("different0123456789abcdef0123456789abcdef0123456789", false, "", logger)
// Get session using mismatched manager/request to force error
_, err = badSM.GetSession(req)
@@ -68,7 +68,7 @@ func TestSessionPoolMemoryLeak(t *testing.T) {
func TestSessionErrorHandling(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)
}
@@ -95,7 +95,7 @@ func TestSessionErrorHandling(t *testing.T) {
func TestSessionClearAlwaysReturnsToPool(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)
}
@@ -181,7 +181,7 @@ func getPooledObjects(sm *SessionManager) int {
// returned to the pool in various scenarios including normal usage and error paths
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)
}
@@ -344,7 +344,7 @@ func TestTokenCompressionCorruptionDetection(t *testing.T) {
// TestTokenChunkingIntegrity tests that large tokens are properly chunked and reassembled
func TestTokenChunkingIntegrity(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)
}
@@ -419,7 +419,7 @@ func TestTokenChunkingIntegrity(t *testing.T) {
// TestTokenChunkingCorruptionResistance tests handling of corrupted chunks
func TestTokenChunkingCorruptionResistance(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)
}
@@ -525,7 +525,7 @@ func TestTokenChunkingCorruptionResistance(t *testing.T) {
// TestTokenSizeLimits tests that token size limits are enforced
func TestTokenSizeLimits(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)
}
@@ -592,7 +592,7 @@ func TestTokenSizeLimits(t *testing.T) {
// TestConcurrentTokenOperations tests thread safety of token operations
func TestConcurrentTokenOperations(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)
}
@@ -646,7 +646,7 @@ func TestConcurrentTokenOperations(t *testing.T) {
// TestSessionValidationAndCleanup tests session validation and orphan cleanup
func TestSessionValidationAndCleanup(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)
}
@@ -694,7 +694,7 @@ func TestSessionValidationAndCleanup(t *testing.T) {
// TestLargeIDTokenChunking tests that large ID tokens are properly chunked across multiple cookies
func TestLargeIDTokenChunking(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)
}
+1
View File
@@ -43,6 +43,7 @@ type Config struct {
AllowedUsers []string `json:"allowedUsers"`
Scopes []string `json:"scopes"`
Headers []TemplatedHeader `json:"headers"`
CookieDomain string `json:"cookieDomain"`
RateLimit int `json:"rateLimit"`
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
ForceHTTPS bool `json:"forceHTTPS"`
+2 -2
View File
@@ -85,7 +85,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) {
@@ -217,7 +217,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)
+4 -4
View File
@@ -17,7 +17,7 @@ import (
// Token verified successfully multiple times, then fails with "signature verification failed"
func TestTokenCorruptionScenario(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)
}
@@ -180,7 +180,7 @@ func TestCompressionIntegrityFailure(t *testing.T) {
// TestChunkReassemblyEdgeCases tests edge cases in chunk reassembly that could cause corruption
func TestChunkReassemblyEdgeCases(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)
}
@@ -296,7 +296,7 @@ func TestChunkReassemblyEdgeCases(t *testing.T) {
// TestRaceConditionProtection tests that concurrent access doesn't cause corruption
func TestRaceConditionProtection(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)
}
@@ -432,7 +432,7 @@ func TestMemoryExhaustionProtection(t *testing.T) {
// TestBackwardCompatibility ensures that sessions created before the fixes still work
func TestBackwardCompatibility(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)
}
+5 -5
View File
@@ -247,7 +247,7 @@ func TestSessionIDTokenAccessToken(t *testing.T) {
logger := NewLogger("debug")
// Create a session manager
sessionManager, err := NewSessionManager("test-session-encryption-key-at-least-32-bytes", false, logger)
sessionManager, err := NewSessionManager("test-session-encryption-key-at-least-32-bytes", false, "", logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
@@ -318,7 +318,7 @@ func TestSessionIDTokenAccessToken(t *testing.T) {
// TestTokenCorruptionIntegrationFlows tests the complete token handling flow with corruption scenarios
func TestTokenCorruptionIntegrationFlows(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)
}
@@ -442,7 +442,7 @@ func TestTokenCorruptionIntegrationFlows(t *testing.T) {
// TestSessionPersistenceWithCorruption tests that session corruption is handled across requests
func TestSessionPersistenceWithCorruption(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)
}
@@ -506,7 +506,7 @@ func TestSessionPersistenceWithCorruption(t *testing.T) {
// TestConcurrentTokenOperationsWithCorruption tests concurrent access with intentional corruption
func TestConcurrentTokenOperationsWithCorruption(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)
}
@@ -592,7 +592,7 @@ func TestConcurrentTokenOperationsWithCorruption(t *testing.T) {
// TestTokenValidationEdgeCases tests edge cases in token validation
func TestTokenValidationEdgeCases(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)
}