mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-16 04:29:15 +00:00
CSRF and Cookie Domain fixes
This commit is contained in:
@@ -77,6 +77,7 @@ testData:
|
|||||||
revocationURL: https://accounts.google.com/revoke # Endpoint for revoking tokens
|
revocationURL: https://accounts.google.com/revoke # Endpoint for revoking tokens
|
||||||
oidcEndSessionURL: https://accounts.google.com/logout # Provider's end session endpoint
|
oidcEndSessionURL: https://accounts.google.com/logout # Provider's end session endpoint
|
||||||
enablePKCE: false # Enables PKCE (Proof Key for Code Exchange) for additional security
|
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 ---
|
# --- Provider Specific Configuration Examples ---
|
||||||
#
|
#
|
||||||
@@ -373,6 +374,31 @@ configuration:
|
|||||||
Default: false
|
Default: false
|
||||||
required: 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:
|
headers:
|
||||||
type: array
|
type: array
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -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` |
|
| `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` |
|
| `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` |
|
| `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 |
|
| `headers` | Custom HTTP headers with templates that can access OIDC claims and tokens | none | See "Templated Headers" section |
|
||||||
|
|
||||||
## Scope Configuration
|
## Scope Configuration
|
||||||
@@ -311,6 +312,30 @@ spec:
|
|||||||
- developer
|
- 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
|
### With Custom Logging and Rate Limiting
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// during the callback from Azure AD
|
// during the callback from Azure AD
|
||||||
func TestAzureOAuthCallbackScenario(t *testing.T) {
|
func TestAzureOAuthCallbackScenario(t *testing.T) {
|
||||||
t.Run("Azure_OAuth_Complete_Flow", func(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Step 1: User visits https://app.example.com/protected
|
// 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) {
|
t.Run("Cookie_Not_Sent_With_Wrong_Domain", func(t *testing.T) {
|
||||||
// This test verifies that cookies with wrong domain won't be sent
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Initial request sets cookie for app.example.com
|
// 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
|
// 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
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// If we had SameSite=Strict (which we don't anymore), the browser would:
|
// If we had SameSite=Strict (which we don't anymore), the browser would:
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@ func TestAzureOIDCRegression(t *testing.T) {
|
|||||||
tOidc := &mockTraefikOidc{TraefikOidc: baseOidc}
|
tOidc := &mockTraefikOidc{TraefikOidc: baseOidc}
|
||||||
|
|
||||||
// Initialize session manager
|
// 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
|
tOidc.sessionManager = sessionManager
|
||||||
|
|
||||||
// Mock the JWT verification to avoid JWKS lookup issues
|
// Mock the JWT verification to avoid JWKS lookup issues
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) {
|
|||||||
// Test that CSRF tokens persist through the authentication flow
|
// Test that CSRF tokens persist through the authentication flow
|
||||||
t.Run("CSRF_Token_Persists_After_Selective_Clear", func(t *testing.T) {
|
t.Run("CSRF_Token_Persists_After_Selective_Clear", func(t *testing.T) {
|
||||||
// Create a session manager
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create initial request
|
// Create initial request
|
||||||
@@ -90,7 +90,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) {
|
|||||||
|
|
||||||
// Test that marking session as dirty forces save
|
// Test that marking session as dirty forces save
|
||||||
t.Run("Mark_Dirty_Forces_Session_Save", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com/test", nil)
|
req := httptest.NewRequest("GET", "http://example.com/test", nil)
|
||||||
@@ -126,7 +126,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) {
|
|||||||
|
|
||||||
// Test Azure-specific session handling
|
// Test Azure-specific session handling
|
||||||
t.Run("Azure_Session_Cookie_Configuration", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Simulate Azure callback scenario
|
// Simulate Azure callback scenario
|
||||||
@@ -158,7 +158,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) {
|
|||||||
|
|
||||||
// Test session continuity through auth flow
|
// Test session continuity through auth flow
|
||||||
t.Run("Session_Continuity_Through_Auth_Flow", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Step 1: Initial request
|
// Step 1: Initial request
|
||||||
@@ -199,7 +199,7 @@ func TestCSRFTokenSessionManagement(t *testing.T) {
|
|||||||
|
|
||||||
// Test large token handling doesn't affect CSRF
|
// Test large token handling doesn't affect CSRF
|
||||||
t.Run("Large_Tokens_Dont_Affect_CSRF", func(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com/test", nil)
|
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,
|
// We can't fully initialize TraefikOidc without network access,
|
||||||
// but we can test the session management directly
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("Session_Created_On_Protected_Request", func(t *testing.T) {
|
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
|
// TestRegressionLoginLoop specifically tests the fix for issue #53
|
||||||
func TestRegressionLoginLoop(t *testing.T) {
|
func TestRegressionLoginLoop(t *testing.T) {
|
||||||
// This test verifies that the specific changes made to fix the login loop work correctly
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Simulate the exact flow that was causing the login loop
|
// 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
|
// TestCSRFValidationTiming tests timing-sensitive CSRF validation scenarios
|
||||||
func TestCSRFValidationTiming(t *testing.T) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("Rapid_Redirect_Maintains_CSRF", func(t *testing.T) {
|
t.Run("Rapid_Redirect_Maintains_CSRF", func(t *testing.T) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestGoogleOIDCRefreshTokenHandling(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a session manager
|
// Create a session manager
|
||||||
sessionManager, _ := NewSessionManager("0123456789abcdef0123456789abcdef", true, mockLogger)
|
sessionManager, _ := NewSessionManager("0123456789abcdef0123456789abcdef", true, "", mockLogger)
|
||||||
tOidc.sessionManager = sessionManager
|
tOidc.sessionManager = sessionManager
|
||||||
|
|
||||||
t.Run("Google provider detection adds required parameters", func(t *testing.T) {
|
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
|
// Create a session manager
|
||||||
sessionManager, _ := NewSessionManager("0123456789abcdef0123456789abcdef", true, mockLogger)
|
sessionManager, _ := NewSessionManager("0123456789abcdef0123456789abcdef", true, "", mockLogger)
|
||||||
googleTOidc.sessionManager = sessionManager
|
googleTOidc.sessionManager = sessionManager
|
||||||
|
|
||||||
// Create a mock token verifier
|
// Create a mock token verifier
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestIssue53Regression(t *testing.T) {
|
|||||||
// 3. Session cookies must be properly configured for HTTPS
|
// 3. Session cookies must be properly configured for HTTPS
|
||||||
// 4. CSRF token must persist through the OAuth flow
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Step 1: Initial request to protected resource
|
// 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,
|
// This test ensures that once the session is properly maintained,
|
||||||
// token signature verification works correctly for Azure tokens
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create authenticated session with Azure tokens
|
// Create authenticated session with Azure tokens
|
||||||
@@ -154,7 +154,7 @@ func TestIssue53Regression(t *testing.T) {
|
|||||||
// This test verifies the redirect loop prevention mechanism
|
// This test verifies the redirect loop prevention mechanism
|
||||||
// that was added to handle authentication failures gracefully
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://internal/protected", nil)
|
req := httptest.NewRequest("GET", "http://internal/protected", nil)
|
||||||
@@ -183,7 +183,7 @@ func TestIssue53Regression(t *testing.T) {
|
|||||||
// in different reverse proxy scenarios
|
// in different reverse proxy scenarios
|
||||||
func TestReverseProxySameSiteHandling(t *testing.T) {
|
func TestReverseProxySameSiteHandling(t *testing.T) {
|
||||||
t.Run("SameSite_Lax_For_HTTPS_OAuth", func(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// HTTPS request via proxy
|
// HTTPS request via proxy
|
||||||
@@ -211,7 +211,7 @@ func TestReverseProxySameSiteHandling(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SameSite_Lax_For_HTTP", func(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Plain HTTP request (no proxy headers)
|
// Plain HTTP request (no proxy headers)
|
||||||
|
|||||||
@@ -756,7 +756,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
suppressDiagnosticLogs: isTestMode(), // Suppress diagnostic logs during tests
|
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.errorRecoveryManager = NewErrorRecoveryManager(t.logger)
|
||||||
t.extractClaimsFunc = extractClaims
|
t.extractClaimsFunc = extractClaims
|
||||||
// t.exchangeCodeForTokenFunc = t.exchangeCodeForToken // Removed, using interface now
|
// t.exchangeCodeForTokenFunc = t.exchangeCodeForToken // Removed, using interface now
|
||||||
@@ -1087,6 +1087,10 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
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
|
// Retrieve or create session for request
|
||||||
session, err := t.sessionManager.GetSession(req)
|
session, err := t.sessionManager.GetSession(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+4
-4
@@ -90,7 +90,7 @@ func (ts *TestSuite) Setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger := NewLogger("info")
|
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
|
// Common TraefikOidc instance
|
||||||
ts.tOidc = &TraefikOidc{
|
ts.tOidc = &TraefikOidc{
|
||||||
@@ -1173,7 +1173,7 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
ts.tOidc.tokenBlacklist = NewCache() // Use generic cache for blacklist
|
ts.tOidc.tokenBlacklist = NewCache() // Use generic cache for blacklist
|
||||||
|
|
||||||
logger := NewLogger("info")
|
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
|
// Create a new instance for each test to avoid state carryover
|
||||||
instanceExtractClaimsFunc := tc.extractClaimsFunc
|
instanceExtractClaimsFunc := tc.extractClaimsFunc
|
||||||
@@ -1561,7 +1561,7 @@ func TestHandleLogout(t *testing.T) {
|
|||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
logger := NewLogger("info")
|
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{
|
tOidc := &TraefikOidc{
|
||||||
revocationURL: mockRevocationServer.URL,
|
revocationURL: mockRevocationServer.URL,
|
||||||
endSessionURL: tc.endSessionURL,
|
endSessionURL: tc.endSessionURL,
|
||||||
@@ -1863,7 +1863,7 @@ func TestHandleExpiredToken(t *testing.T) {
|
|||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
logger := NewLogger("info")
|
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{
|
tOidc := &TraefikOidc{
|
||||||
sessionManager: sessionManager,
|
sessionManager: sessionManager,
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ func TestMemoryLeakFixes(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Session pool returns properly", func(t *testing.T) {
|
t.Run("Session pool returns properly", func(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
// TestReverseProxyHTTPSDetection tests that HTTPS is properly detected in reverse proxy environments
|
// TestReverseProxyHTTPSDetection tests that HTTPS is properly detected in reverse proxy environments
|
||||||
func TestReverseProxyHTTPSDetection(t *testing.T) {
|
func TestReverseProxyHTTPSDetection(t *testing.T) {
|
||||||
t.Run("HTTPS_Detection_With_X_Forwarded_Proto", func(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Simulate request from reverse proxy (Traefik/nginx)
|
// 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) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Request without reverse proxy headers (direct HTTP)
|
// 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) {
|
t.Run("HTTPS_Detection_With_ForceHTTPS", func(t *testing.T) {
|
||||||
// Test with forceHTTPS enabled
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Even without headers, forceHTTPS should make cookies secure
|
// 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
|
// TestCSRFPersistenceInReverseProxy tests CSRF token persistence in reverse proxy setups
|
||||||
func TestCSRFPersistenceInReverseProxy(t *testing.T) {
|
func TestCSRFPersistenceInReverseProxy(t *testing.T) {
|
||||||
t.Run("CSRF_Persists_Through_OAuth_Flow_With_Proxy", func(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Step 1: Initial request to protected resource (HTTPS via proxy)
|
// 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) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test with X-Forwarded-Host header
|
// Test with X-Forwarded-Host header
|
||||||
@@ -196,7 +196,7 @@ func TestAzureOIDCWithReverseProxy(t *testing.T) {
|
|||||||
// This test verifies Azure-specific provider detection and configuration
|
// This test verifies Azure-specific provider detection and configuration
|
||||||
// without making actual network calls
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Step 1: Test session setup for Azure OAuth flow
|
// 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) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Simulate a scenario where some requests come via HTTPS proxy and some don't
|
// 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
|
// TestEnhanceSessionSecurity verifies the security enhancement function
|
||||||
func TestEnhanceSessionSecurity(t *testing.T) {
|
func TestEnhanceSessionSecurity(t *testing.T) {
|
||||||
t.Run("Security_Enhancement_For_AJAX_Requests", func(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// AJAX request via HTTPS proxy
|
// 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) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Request without User-Agent (potential bot/attack)
|
// Request without User-Agent (potential bot/attack)
|
||||||
|
|||||||
+1
-1
@@ -197,7 +197,7 @@ func TestCacheMemoryExhaustion(t *testing.T) {
|
|||||||
// TestSessionConcurrencyProtection tests session safety under concurrent access
|
// TestSessionConcurrencyProtection tests session safety under concurrent access
|
||||||
func TestSessionConcurrencyProtection(t *testing.T) {
|
func TestSessionConcurrencyProtection(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ func TestMissingClaims(t *testing.T) {
|
|||||||
// TestSessionFixationAttack tests the plugin's resistance to session fixation attacks
|
// TestSessionFixationAttack tests the plugin's resistance to session fixation attacks
|
||||||
func TestSessionFixationAttack(t *testing.T) {
|
func TestSessionFixationAttack(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestCSRFProtection tests CSRF protection in POST requests
|
||||||
func TestCSRFProtection(t *testing.T) {
|
func TestCSRFProtection(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-6
@@ -243,17 +243,20 @@ type SessionManager struct {
|
|||||||
store sessions.Store
|
store sessions.Store
|
||||||
logger *Logger
|
logger *Logger
|
||||||
forceHTTPS bool
|
forceHTTPS bool
|
||||||
|
cookieDomain string
|
||||||
chunkManager *ChunkManager
|
chunkManager *ChunkManager
|
||||||
|
cleanupDone bool // Track if we've attempted cookie cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSessionManager creates a new session manager with the specified configuration.
|
// NewSessionManager creates a new session manager with the specified configuration.
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - encryptionKey: Key used to encrypt session data (must be at least 32 bytes)
|
// - encryptionKey: Key used to encrypt session data (must be at least 32 bytes)
|
||||||
// - forceHTTPS: When true, forces secure cookie attributes regardless of request scheme
|
// - 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
|
// - logger: Logger instance for recording session-related events
|
||||||
//
|
//
|
||||||
// Returns an error if the encryption key does not meet minimum length requirements.
|
// 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.
|
// Validate encryption key length.
|
||||||
if len(encryptionKey) < minEncryptionKeyLength {
|
if len(encryptionKey) < minEncryptionKeyLength {
|
||||||
return nil, fmt.Errorf("encryption key must be at least %d bytes long", 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{
|
sm := &SessionManager{
|
||||||
store: sessions.NewCookieStore([]byte(encryptionKey)),
|
store: sessions.NewCookieStore([]byte(encryptionKey)),
|
||||||
forceHTTPS: forceHTTPS,
|
forceHTTPS: forceHTTPS,
|
||||||
|
cookieDomain: cookieDomain,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
chunkManager: NewChunkManager(logger),
|
chunkManager: NewChunkManager(logger),
|
||||||
}
|
}
|
||||||
@@ -512,8 +516,13 @@ func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *h
|
|||||||
// Always enforce security best practices
|
// Always enforce security best practices
|
||||||
options.HttpOnly = true
|
options.HttpOnly = true
|
||||||
|
|
||||||
// Set secure Domain attribute for production
|
// Set cookie domain from configuration if specified
|
||||||
if options.Domain == "" && r != nil {
|
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
|
// In reverse proxy setups, use the original host if available
|
||||||
// This ensures cookies work correctly with the public domain
|
// This ensures cookies work correctly with the public domain
|
||||||
host := r.Host
|
host := r.Host
|
||||||
@@ -529,10 +538,10 @@ func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *h
|
|||||||
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
|
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
|
||||||
host = host[:colonIndex] // Remove port
|
host = host[:colonIndex] // Remove port
|
||||||
}
|
}
|
||||||
// For cookie domain, we might want to use the base domain
|
// Use the exact host (without leading dot) for host-only cookies
|
||||||
// to allow cookies to work across subdomains
|
// This prevents subdomain access but ensures consistency
|
||||||
// But for now, use the exact host for security
|
|
||||||
options.Domain = host
|
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,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: int(absoluteSessionTimeout.Seconds()),
|
MaxAge: int(absoluteSessionTimeout.Seconds()),
|
||||||
Path: "/",
|
Path: "/",
|
||||||
|
Domain: sm.cookieDomain, // Use configured domain if set
|
||||||
}
|
}
|
||||||
return baseOptions
|
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.
|
// GetSession retrieves all session data for the current request.
|
||||||
// It loads the main session and token sessions, including any chunked token data,
|
// It loads the main session and token sessions, including any chunked token data,
|
||||||
// and combines them into a single SessionData structure for easy access.
|
// and combines them into a single SessionData structure for easy access.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
func TestSessionCompatibilityAfterChanges(t *testing.T) {
|
func TestSessionCompatibilityAfterChanges(t *testing.T) {
|
||||||
t.Run("Plain_HTTP_Without_Proxy_Headers", func(t *testing.T) {
|
t.Run("Plain_HTTP_Without_Proxy_Headers", func(t *testing.T) {
|
||||||
// Test that plain HTTP requests without proxy headers still work
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://localhost:8080/test", nil)
|
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) {
|
t.Run("HTTPS_With_TLS_Field", func(t *testing.T) {
|
||||||
// Test direct HTTPS connection (not proxied)
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
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) {
|
t.Run("ForceHTTPS_Setting", func(t *testing.T) {
|
||||||
// Test forceHTTPS setting works regardless of request
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Plain HTTP request
|
// Plain HTTP request
|
||||||
@@ -132,7 +132,7 @@ func TestSessionCompatibilityAfterChanges(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("AJAX_Request_Gets_Strict_SameSite", func(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// AJAX request
|
// AJAX request
|
||||||
@@ -165,7 +165,7 @@ func TestSessionCompatibilityAfterChanges(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Missing_UserAgent_Gets_Reduced_Timeout", func(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Request without User-Agent (suspicious)
|
// Request without User-Agent (suspicious)
|
||||||
|
|||||||
+11
-11
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
func TestSessionPoolMemoryLeak(t *testing.T) {
|
func TestSessionPoolMemoryLeak(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func TestSessionPoolMemoryLeak(t *testing.T) {
|
|||||||
|
|
||||||
// Test 3: Error path in GetSession
|
// Test 3: Error path in GetSession
|
||||||
// Modify the session store to force an error - use a different encryption key
|
// 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
|
// Get session using mismatched manager/request to force error
|
||||||
_, err = badSM.GetSession(req)
|
_, err = badSM.GetSession(req)
|
||||||
@@ -68,7 +68,7 @@ func TestSessionPoolMemoryLeak(t *testing.T) {
|
|||||||
|
|
||||||
func TestSessionErrorHandling(t *testing.T) {
|
func TestSessionErrorHandling(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ func TestSessionErrorHandling(t *testing.T) {
|
|||||||
|
|
||||||
func TestSessionClearAlwaysReturnsToPool(t *testing.T) {
|
func TestSessionClearAlwaysReturnsToPool(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// returned to the pool in various scenarios including normal usage and error paths
|
||||||
func TestSessionObjectTracking(t *testing.T) {
|
func TestSessionObjectTracking(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestTokenChunkingIntegrity tests that large tokens are properly chunked and reassembled
|
||||||
func TestTokenChunkingIntegrity(t *testing.T) {
|
func TestTokenChunkingIntegrity(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
@@ -419,7 +419,7 @@ func TestTokenChunkingIntegrity(t *testing.T) {
|
|||||||
// TestTokenChunkingCorruptionResistance tests handling of corrupted chunks
|
// TestTokenChunkingCorruptionResistance tests handling of corrupted chunks
|
||||||
func TestTokenChunkingCorruptionResistance(t *testing.T) {
|
func TestTokenChunkingCorruptionResistance(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestTokenSizeLimits tests that token size limits are enforced
|
||||||
func TestTokenSizeLimits(t *testing.T) {
|
func TestTokenSizeLimits(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestConcurrentTokenOperations tests thread safety of token operations
|
||||||
func TestConcurrentTokenOperations(t *testing.T) {
|
func TestConcurrentTokenOperations(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestSessionValidationAndCleanup tests session validation and orphan cleanup
|
||||||
func TestSessionValidationAndCleanup(t *testing.T) {
|
func TestSessionValidationAndCleanup(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestLargeIDTokenChunking tests that large ID tokens are properly chunked across multiple cookies
|
||||||
func TestLargeIDTokenChunking(t *testing.T) {
|
func TestLargeIDTokenChunking(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type Config struct {
|
|||||||
AllowedUsers []string `json:"allowedUsers"`
|
AllowedUsers []string `json:"allowedUsers"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
Headers []TemplatedHeader `json:"headers"`
|
Headers []TemplatedHeader `json:"headers"`
|
||||||
|
CookieDomain string `json:"cookieDomain"`
|
||||||
RateLimit int `json:"rateLimit"`
|
RateLimit int `json:"rateLimit"`
|
||||||
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
||||||
ForceHTTPS bool `json:"forceHTTPS"`
|
ForceHTTPS bool `json:"forceHTTPS"`
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httpt
|
|||||||
logInfo: log.New(&testWriter{t}, "INFO: ", 0),
|
logInfo: log.New(&testWriter{t}, "INFO: ", 0),
|
||||||
logDebug: log.New(&testWriter{t}, "DEBUG: ", 0),
|
logDebug: log.New(&testWriter{t}, "DEBUG: ", 0),
|
||||||
}
|
}
|
||||||
sessionManager, _ := NewSessionManager(config.SessionEncryptionKey, false, logger)
|
sessionManager, _ := NewSessionManager(config.SessionEncryptionKey, false, "", logger)
|
||||||
|
|
||||||
// Create next handler
|
// Create next handler
|
||||||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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 {
|
func createTestSession() *SessionData {
|
||||||
// Create a minimal session manager for testing
|
// Create a minimal session manager for testing
|
||||||
logger := newNoOpLogger()
|
logger := newNoOpLogger()
|
||||||
sessionManager, _ := NewSessionManager("test-encryption-key-32-characters", false, logger)
|
sessionManager, _ := NewSessionManager("test-encryption-key-32-characters", false, "", logger)
|
||||||
|
|
||||||
// Create a test request
|
// Create a test request
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// Token verified successfully multiple times, then fails with "signature verification failed"
|
// Token verified successfully multiple times, then fails with "signature verification failed"
|
||||||
func TestTokenCorruptionScenario(t *testing.T) {
|
func TestTokenCorruptionScenario(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestChunkReassemblyEdgeCases tests edge cases in chunk reassembly that could cause corruption
|
||||||
func TestChunkReassemblyEdgeCases(t *testing.T) {
|
func TestChunkReassemblyEdgeCases(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestRaceConditionProtection tests that concurrent access doesn't cause corruption
|
||||||
func TestRaceConditionProtection(t *testing.T) {
|
func TestRaceConditionProtection(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestBackwardCompatibility ensures that sessions created before the fixes still work
|
||||||
func TestBackwardCompatibility(t *testing.T) {
|
func TestBackwardCompatibility(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ func TestSessionIDTokenAccessToken(t *testing.T) {
|
|||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
|
|
||||||
// Create a session manager
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestTokenCorruptionIntegrationFlows tests the complete token handling flow with corruption scenarios
|
||||||
func TestTokenCorruptionIntegrationFlows(t *testing.T) {
|
func TestTokenCorruptionIntegrationFlows(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestSessionPersistenceWithCorruption tests that session corruption is handled across requests
|
||||||
func TestSessionPersistenceWithCorruption(t *testing.T) {
|
func TestSessionPersistenceWithCorruption(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestConcurrentTokenOperationsWithCorruption tests concurrent access with intentional corruption
|
||||||
func TestConcurrentTokenOperationsWithCorruption(t *testing.T) {
|
func TestConcurrentTokenOperationsWithCorruption(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
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
|
// TestTokenValidationEdgeCases tests edge cases in token validation
|
||||||
func TestTokenValidationEdgeCases(t *testing.T) {
|
func TestTokenValidationEdgeCases(t *testing.T) {
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, logger)
|
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user