From 6efb78b7a885d5b1f23c42b994163ad1025d580f Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Fri, 12 Dec 2025 18:35:06 +0000 Subject: [PATCH] Smarter approach to the cookies (#103) * Smarter approach to the cookies - Single maxCookieSize = 1400 constant with clear documentation - Combined cookie storage for ~40-45% size reduction - Backward compatible migration from legacy cookies * Tuneup the code. --- audience_test.go | 8 +- auth_flow_behaviour_test.go | 2 +- autocleanup.go | 21 +- azure_oidc_test.go | 12 +- background_tasks_ultra_test.go | 14 +- cache_compat.go | 6 +- cache_test.go | 22 +- dynamic_client_registration.go | 57 +- dynamic_client_registration_test.go | 10 +- enhanced_mocks_test.go | 76 +-- error_recovery.go | 51 +- error_recovery_test.go | 18 +- goroutine_manager.go | 12 +- http_client_factory.go | 31 +- http_client_factory_unit_test.go | 2 +- http_client_pool.go | 12 +- input_validation_test.go | 18 +- integration/integration_consolidated_test.go | 8 +- internal/cache/backends/config.go | 39 +- internal/cache/backends/hybrid.go | 46 +- internal/cache/backends/hybrid_test.go | 12 +- internal/cache/backends/interface.go | 59 +- internal/cache/backends/memory.go | 62 +- internal/cache/backends/redis_health.go | 26 +- internal/cache/backends/resp_test.go | 10 +- internal/cache/cache.go | 68 +-- internal/cache/cache_test.go | 22 +- internal/cache/manager.go | 9 +- internal/cache/resilience/circuit_breaker.go | 65 +- .../circuit_breaker_backend_test.go | 2 +- internal/cache/resilience/health_check.go | 77 +-- .../cache/resilience/health_check_backend.go | 18 +- internal/cache/typed_cache.go | 4 +- internal/cleanup/cleanup_test.go | 2 +- internal/cleanup/manager.go | 22 +- internal/cleanup/workers.go | 26 +- internal/features/flags.go | 4 +- internal/pool/transport.go | 47 +- internal/providers/azure_test.go | 8 +- internal/providers/base_test.go | 10 +- internal/providers/factory_test.go | 4 +- internal/providers/generic_test.go | 4 +- internal/providers/google_test.go | 2 +- internal/providers/registry_test.go | 6 +- internal/providers/validation_test.go | 18 +- internal/providers/warnings.go | 4 +- internal/providers/warnings_test.go | 2 +- internal/recovery/base.go | 22 +- internal/recovery/circuit_breaker.go | 20 +- internal/recovery/metrics.go | 25 +- internal/recovery/metrics_test.go | 16 +- internal/recovery/recovery_test.go | 12 +- internal/testutil/mocks/interfaces.go | 10 +- internal/testutil/mocks/session.go | 4 +- internal/testutil/servers/oidc.go | 63 +- internal/utils/utils_test.go | 8 +- jwk.go | 27 +- jwk_caching_test.go | 18 +- main_exchange_test.go | 6 +- main_goroutine_leak_test.go | 4 +- main_initialization_test.go | 4 +- main_refresh_test.go | 6 +- main_servehttp_test.go | 8 +- main_simple_test.go | 4 +- main_test.go | 4 +- memory_leak_test.go | 22 +- memory_monitor.go | 66 +- mocks_test.go | 85 +-- refresh_coordinator.go | 81 +-- regression/regression_test.go | 8 +- session.go | 578 +++++++++++++++++- session/chunking/chunk_manager.go | 9 +- session/chunking/chunk_manager_test.go | 42 +- session/chunking/chunk_serializer.go | 8 +- session_chunk_manager.go | 21 +- session_test.go | 30 +- settings.go | 437 ++++--------- sharded_cache.go | 2 +- singleton_resources.go | 51 +- test_config.go | 33 +- test_framework_test.go | 8 +- test_infrastructure.go | 30 +- test_utils_test.go | 2 +- token_introspection.go | 24 +- token_resilience.go | 36 +- token_test.go | 50 +- token_validator.go | 6 +- types.go | 68 +-- universal_cache.go | 78 +-- universal_cache_singleton.go | 24 +- 90 files changed, 1529 insertions(+), 1589 deletions(-) diff --git a/audience_test.go b/audience_test.go index c61aa04..f805ab0 100644 --- a/audience_test.go +++ b/audience_test.go @@ -84,8 +84,8 @@ func TestAudienceValidation(t *testing.T) { tests := []struct { name string audience string - expectError bool errorContains string + expectError bool }{ { name: "valid custom audience URL", @@ -163,8 +163,8 @@ func TestConfigAudienceValidation(t *testing.T) { tests := []struct { name string audience string - wantErr bool errContains string + wantErr bool }{ { name: "Empty audience is valid for backward compatibility", @@ -732,11 +732,11 @@ func TestJWTAudienceVerification(t *testing.T) { tokenCache := tc.addTokenCache(NewTokenCache()) tests := []struct { + tokenAudience interface{} name string configAudience string - tokenAudience interface{} - wantErr bool errContains string + wantErr bool skipReplayCheck bool }{ { diff --git a/auth_flow_behaviour_test.go b/auth_flow_behaviour_test.go index 2b92c35..b39af6a 100644 --- a/auth_flow_behaviour_test.go +++ b/auth_flow_behaviour_test.go @@ -253,8 +253,8 @@ func (s *AuthFlowBehaviourSuite) TestPrepareSessionForAuthentication_WithPKCE() // TestIsAjaxRequest tests AJAX request detection func (s *AuthFlowBehaviourSuite) TestIsAjaxRequest() { testCases := []struct { - name string headers map[string]string + name string expectAjax bool }{ { diff --git a/autocleanup.go b/autocleanup.go index ecba081..39764de 100644 --- a/autocleanup.go +++ b/autocleanup.go @@ -222,17 +222,16 @@ func (bt *BackgroundTask) run() { // TaskCircuitBreaker implements circuit breaker pattern for background task creation // It limits concurrent task execution and tracks failures to prevent system overload type TaskCircuitBreaker struct { - state int32 // CircuitBreakerState - failureCount int32 - lastFailureTime int64 // Unix timestamp - failureThreshold int32 - timeout time.Duration logger *Logger - // Concurrency limiting - concurrentTasks int32 // Current number of running tasks - maxConcurrent int32 // Maximum concurrent tasks allowed - activeTasks map[string]struct{} // Track active task names - tasksMu sync.RWMutex // Separate mutex for task tracking + activeTasks map[string]struct{} + lastFailureTime int64 + timeout time.Duration + tasksMu sync.RWMutex + state int32 + failureCount int32 + failureThreshold int32 + concurrentTasks int32 + maxConcurrent int32 } // NewTaskCircuitBreaker creates a new circuit breaker for background tasks @@ -380,9 +379,9 @@ func (cb *TaskCircuitBreaker) OnTaskFailure(taskName string, err error) { // TaskRegistry maintains a registry of all active background tasks to prevent duplicates type TaskRegistry struct { tasks map[string]*BackgroundTask - mu sync.RWMutex cb *TaskCircuitBreaker logger *Logger + mu sync.RWMutex } // GlobalTaskRegistry is the singleton instance for managing all background tasks diff --git a/azure_oidc_test.go b/azure_oidc_test.go index f511fb8..d7106fc 100644 --- a/azure_oidc_test.go +++ b/azure_oidc_test.go @@ -330,12 +330,12 @@ func TestValidateGoogleTokens(t *testing.T) { ts.tOidc.refreshGracePeriod = 60 * time.Second tests := []struct { - name string setupSession func() *SessionData + name string + description string expectedAuth bool expectedRefresh bool expectedExpired bool - description string }{ { name: "ValidGoogleTokens", @@ -476,13 +476,13 @@ func TestIsUserAuthenticated(t *testing.T) { ts.tOidc.refreshGracePeriod = 60 * time.Second tests := []struct { + setupSession func() *SessionData name string providerType string - setupSession func() *SessionData + description string expectedAuth bool expectedRefresh bool expectedExpired bool - description string }{ { name: "AzureProvider", @@ -660,12 +660,12 @@ func TestValidateAzureTokensEdgeCases(t *testing.T) { ts.tOidc.refreshGracePeriod = 60 * time.Second tests := []struct { - name string setupSession func() *SessionData + name string + description string expectedAuth bool expectedRefresh bool expectedExpired bool - description string }{ { name: "UnauthenticatedWithRefreshToken", diff --git a/background_tasks_ultra_test.go b/background_tasks_ultra_test.go index 42d2c99..c9b32bc 100644 --- a/background_tasks_ultra_test.go +++ b/background_tasks_ultra_test.go @@ -97,15 +97,15 @@ func TestMemoryMonitorComprehensive(t *testing.T) { t.Run("String method returns pressure name", func(t *testing.T) { pressures := []struct { - level MemoryPressureLevel name string + level MemoryPressureLevel }{ - {MemoryPressureNone, "None"}, - {MemoryPressureLow, "Low"}, - {MemoryPressureModerate, "Moderate"}, - {MemoryPressureHigh, "High"}, - {MemoryPressureCritical, "Critical"}, - {MemoryPressureLevel(999), "Unknown"}, + {level: MemoryPressureNone, name: "None"}, + {level: MemoryPressureLow, name: "Low"}, + {level: MemoryPressureModerate, name: "Moderate"}, + {level: MemoryPressureHigh, name: "High"}, + {level: MemoryPressureCritical, name: "Critical"}, + {level: MemoryPressureLevel(999), name: "Unknown"}, } for _, p := range pressures { diff --git a/cache_compat.go b/cache_compat.go index e64a4dc..8b6118f 100644 --- a/cache_compat.go +++ b/cache_compat.go @@ -155,9 +155,9 @@ type CacheStrategy interface { // CacheEntry for backward compatibility type CacheEntry struct { - Key string - Value interface{} ExpiresAt time.Time + Value interface{} + Key string } // Cache is an alias for backward compatibility @@ -175,10 +175,10 @@ func NewOptimizedCacheWithConfig(config OptimizedCacheConfig) *CacheInterfaceWra // ListNode for backward compatibility type ListNode struct { - Key string Value interface{} Next *ListNode Prev *ListNode + Key string } // NewFixedMetadataCache creates a metadata cache with fixed configuration diff --git a/cache_test.go b/cache_test.go index 5887f2b..80e806e 100644 --- a/cache_test.go +++ b/cache_test.go @@ -19,16 +19,16 @@ import ( // CacheTestCase represents a comprehensive test case for cache operations type CacheTestCase struct { + setup func(*TestFramework) + execute func(*TestFramework) error + validate func(*testing.T, error, *TestFramework) + cleanup func(*TestFramework) name string - cacheType string // "universal", "metadata", "bounded" - operation string // "get", "set", "evict", "cleanup" - setup func(*TestFramework) // Pre-test setup - execute func(*TestFramework) error // Test execution - validate func(*testing.T, error, *TestFramework) // Validation logic - cleanup func(*TestFramework) // Post-test cleanup - timeout time.Duration // Test timeout - parallel bool // Can run in parallel - skipReason string // Optional reason to skip + cacheType string + operation string + skipReason string + timeout time.Duration + parallel bool } // createTestCacheConfig creates a standard test configuration @@ -698,10 +698,10 @@ func TestUnifiedCache_SetMaxSize(t *testing.T) { func TestNewCacheAdapter(t *testing.T) { tests := []struct { - name string cache interface{} - expectNil bool + name string description string + expectNil bool }{ { name: "UniversalCache", diff --git a/dynamic_client_registration.go b/dynamic_client_registration.go index 0fae270..69f754f 100644 --- a/dynamic_client_registration.go +++ b/dynamic_client_registration.go @@ -16,35 +16,26 @@ import ( // ClientRegistrationResponse represents the response from a successful client registration (RFC 7591) type ClientRegistrationResponse struct { - // Required fields - ClientID string `json:"client_id"` - - // Conditional - only for confidential clients - ClientSecret string `json:"client_secret,omitempty"` - - // Optional - for managing registration - RegistrationAccessToken string `json:"registration_access_token,omitempty"` - RegistrationClientURI string `json:"registration_client_uri,omitempty"` - - // Expiration - ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"` - ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"` - - // Echo back of registered metadata - RedirectURIs []string `json:"redirect_uris,omitempty"` - ResponseTypes []string `json:"response_types,omitempty"` - GrantTypes []string `json:"grant_types,omitempty"` - ApplicationType string `json:"application_type,omitempty"` - Contacts []string `json:"contacts,omitempty"` - ClientName string `json:"client_name,omitempty"` - LogoURI string `json:"logo_uri,omitempty"` - ClientURI string `json:"client_uri,omitempty"` - PolicyURI string `json:"policy_uri,omitempty"` - TOSURI string `json:"tos_uri,omitempty"` - JWKSURI string `json:"jwks_uri,omitempty"` SubjectType string `json:"subject_type,omitempty"` - TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` + LogoURI string `json:"logo_uri,omitempty"` + RegistrationAccessToken string `json:"registration_access_token,omitempty"` + RegistrationClientURI string `json:"registration_client_uri,omitempty"` Scope string `json:"scope,omitempty"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` + TOSURI string `json:"tos_uri,omitempty"` + PolicyURI string `json:"policy_uri,omitempty"` + ClientSecret string `json:"client_secret,omitempty"` + ApplicationType string `json:"application_type,omitempty"` + ClientID string `json:"client_id"` + ClientName string `json:"client_name,omitempty"` + JWKSURI string `json:"jwks_uri,omitempty"` + ClientURI string `json:"client_uri,omitempty"` + Contacts []string `json:"contacts,omitempty"` + GrantTypes []string `json:"grant_types,omitempty"` + ResponseTypes []string `json:"response_types,omitempty"` + RedirectURIs []string `json:"redirect_uris,omitempty"` + ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"` + ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"` } // ClientRegistrationError represents an error response from client registration (RFC 7591) @@ -55,14 +46,12 @@ type ClientRegistrationError struct { // DynamicClientRegistrar handles OIDC Dynamic Client Registration (RFC 7591) type DynamicClientRegistrar struct { - httpClient *http.Client - logger *Logger - config *DynamicClientRegistrationConfig - providerURL string - - // Cached registration response - mu sync.RWMutex + httpClient *http.Client + logger *Logger + config *DynamicClientRegistrationConfig registrationResponse *ClientRegistrationResponse + providerURL string + mu sync.RWMutex } // NewDynamicClientRegistrar creates a new dynamic client registrar diff --git a/dynamic_client_registration_test.go b/dynamic_client_registration_test.go index 3ff9e99..1efcf85 100644 --- a/dynamic_client_registration_test.go +++ b/dynamic_client_registration_test.go @@ -223,10 +223,10 @@ func TestRegisterClientWithInitialAccessToken(t *testing.T) { // TestRegisterClientError tests error handling during registration func TestRegisterClientError(t *testing.T) { tests := []struct { - name string serverResponse func(w http.ResponseWriter, r *http.Request) - expectError bool + name string errorContains string + expectError bool }{ { name: "invalid_redirect_uri error", @@ -321,8 +321,8 @@ func TestRegisterClientError(t *testing.T) { // TestRegisterClientDisabled tests that registration fails when not enabled func TestRegisterClientDisabled(t *testing.T) { tests := []struct { - name string dcrConfig *DynamicClientRegistrationConfig + name string }{ { name: "nil config", @@ -521,8 +521,8 @@ func TestCredentialsValidation(t *testing.T) { registrar := NewDynamicClientRegistrar(&http.Client{}, NewLogger("DEBUG"), dcrConfig, "https://example.com") tests := []struct { - name string response *ClientRegistrationResponse + name string expected bool }{ { @@ -584,9 +584,9 @@ func TestCredentialsValidation(t *testing.T) { // TestBuildRegistrationRequest tests the request body construction func TestBuildRegistrationRequest(t *testing.T) { tests := []struct { - name string metadata *ClientRegistrationMetadata expectedFields map[string]interface{} + name string expectError bool }{ { diff --git a/enhanced_mocks_test.go b/enhanced_mocks_test.go index 367693f..874bb89 100644 --- a/enhanced_mocks_test.go +++ b/enhanced_mocks_test.go @@ -12,23 +12,19 @@ import ( // EnhancedMockJWKCache is an improved state-based mock with call tracking type EnhancedMockJWKCache struct { - mu sync.RWMutex - - // State (what to return) - JWKS *JWKSet - Err error - - // Call tracking + Err error + JWKS *JWKSet GetJWKSCalls []JWKSCall + mu sync.RWMutex + getJWKSCallsMu sync.Mutex CleanupCalls int32 CloseCalls int32 - getJWKSCallsMu sync.Mutex } // JWKSCall records parameters from a GetJWKS call type JWKSCall struct { - URL string Timestamp time.Time + URL string } func (m *EnhancedMockJWKCache) GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error) { @@ -108,22 +104,18 @@ func (m *EnhancedMockJWKCache) Reset() { // EnhancedMockTokenVerifier is an improved state-based mock with call tracking type EnhancedMockTokenVerifier struct { - mu sync.RWMutex - - // State (what to return) - can be a fixed error or a function - Err error - VerifyFunc func(token string) error - - // Call tracking + Err error + VerifyFunc func(token string) error VerifyCalls []TokenVerifyCall + mu sync.RWMutex verifyCallsMu sync.Mutex } // TokenVerifyCall records parameters from a VerifyToken call type TokenVerifyCall struct { - Token string Timestamp time.Time Result error + Token string } func (m *EnhancedMockTokenVerifier) VerifyToken(token string) error { @@ -207,49 +199,43 @@ func (m *EnhancedMockTokenVerifier) Reset() { // EnhancedMockTokenExchanger is an improved state-based mock with call tracking type EnhancedMockTokenExchanger struct { - mu sync.RWMutex - - // State (what to return) - ExchangeResponse *TokenResponse - ExchangeErr error - RefreshResponse *TokenResponse RefreshErr error RevokeErr error - - // Optional functions for dynamic behavior + ExchangeErr error ExchangeCodeFunc func(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) + RefreshResponse *TokenResponse + ExchangeResponse *TokenResponse RefreshTokenFunc func(refreshToken string) (*TokenResponse, error) RevokeTokenFunc func(token, tokenType string) error - - // Call tracking - ExchangeCalls []ExchangeCall - RefreshCalls []RefreshCall - RevokeCalls []RevokeCall - exchangeCallsMu sync.Mutex - refreshCallsMu sync.Mutex - revokeCallsMu sync.Mutex + ExchangeCalls []ExchangeCall + RefreshCalls []RefreshCall + RevokeCalls []RevokeCall + mu sync.RWMutex + exchangeCallsMu sync.Mutex + refreshCallsMu sync.Mutex + revokeCallsMu sync.Mutex } // ExchangeCall records parameters from an ExchangeCodeForToken call type ExchangeCall struct { + Timestamp time.Time GrantType string CodeOrToken string RedirectURL string CodeVerifier string - Timestamp time.Time } // RefreshCall records parameters from a GetNewTokenWithRefreshToken call type RefreshCall struct { - RefreshToken string Timestamp time.Time + RefreshToken string } // RevokeCall records parameters from a RevokeTokenWithProvider call type RevokeCall struct { + Timestamp time.Time Token string TokenType string - Timestamp time.Time } func (m *EnhancedMockTokenExchanger) ExchangeCodeForToken(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) { @@ -401,16 +387,12 @@ func (m *EnhancedMockTokenExchanger) Reset() { // EnhancedMockCacheInterface is an improved state-based mock for CacheInterface type EnhancedMockCacheInterface struct { - mu sync.RWMutex - - // Internal storage - data map[string]cacheEntry - maxSize int - - // Call tracking + data map[string]cacheEntry GetCalls []CacheGetCall SetCalls []CacheSetCall DeleteCalls []string + maxSize int + mu sync.RWMutex getCalls sync.Mutex setCalls sync.Mutex deleteCalls sync.Mutex @@ -423,17 +405,17 @@ type cacheEntry struct { // CacheGetCall records parameters from a Get call type CacheGetCall struct { + Timestamp time.Time Key string Found bool - Timestamp time.Time } // CacheSetCall records parameters from a Set call type CacheSetCall struct { - Key string - Value any - TTL time.Duration Timestamp time.Time + Value any + Key string + TTL time.Duration } // NewEnhancedMockCache creates a new enhanced cache mock diff --git a/error_recovery.go b/error_recovery.go index 9300696..642190c 100644 --- a/error_recovery.go +++ b/error_recovery.go @@ -642,14 +642,10 @@ func (e *HTTPError) Error() string { // OIDCError represents OIDC-specific errors with context information. // It provides structured error reporting for authentication and authorization failures. type OIDCError struct { - // Code identifies the specific error type - Code string - // Message provides a human-readable description - Message string - // Context contains additional error context (e.g., provider, session details) + Cause error Context map[string]interface{} - // Cause is the underlying error that caused this error - Cause error + Code string + Message string } // Error returns the string representation of the OIDC error. @@ -669,14 +665,10 @@ func (e *OIDCError) Unwrap() error { // SessionError represents session-related errors with context. // Used for session management, validation, and storage errors. type SessionError struct { - // Operation describes what session operation failed + Cause error Operation string - // Message provides a human-readable description - Message string - // SessionID identifies the session (if available) + Message string SessionID string - // Cause is the underlying error that caused this error - Cause error } // Error returns the string representation of the session error. @@ -696,14 +688,10 @@ func (e *SessionError) Unwrap() error { // TokenError represents token-related errors with validation context. // Used for JWT validation, token refresh, and token format errors. type TokenError struct { - // TokenType identifies the type of token (id_token, access_token, refresh_token) + Cause error TokenType string - // Reason describes why the token is invalid - Reason string - // Message provides a human-readable description - Message string - // Cause is the underlying error that caused this error - Cause error + Reason string + Message string } // Error returns the string representation of the token error. @@ -765,24 +753,15 @@ func NewTokenError(tokenType, reason, message string, cause error) *TokenError { // It provides fallback mechanisms when primary services are unavailable and monitors // service health to automatically recover when services become available again. type GracefulDegradation struct { - // BaseRecoveryMechanism provides common functionality *BaseRecoveryMechanism - // fallbacks stores service-specific fallback implementations - fallbacks map[string]func() (interface{}, error) - // healthChecks stores service health check functions - healthChecks map[string]func() bool - // degradedServices tracks which services are currently degraded + fallbacks map[string]func() (interface{}, error) + healthChecks map[string]func() bool degradedServices map[string]time.Time - // config contains graceful degradation configuration - config GracefulDegradationConfig - // mutex protects shared state - mutex sync.RWMutex - // healthCheckTask manages background health checking - healthCheckTask *BackgroundTask - // stopChan signals shutdown - stopChan chan struct{} - // shutdownOnce ensures shutdown happens only once - shutdownOnce sync.Once + healthCheckTask *BackgroundTask + stopChan chan struct{} + config GracefulDegradationConfig + mutex sync.RWMutex + shutdownOnce sync.Once } // GracefulDegradationConfig holds configuration for graceful degradation behavior. diff --git a/error_recovery_test.go b/error_recovery_test.go index ae66696..393416b 100644 --- a/error_recovery_test.go +++ b/error_recovery_test.go @@ -20,10 +20,10 @@ import ( func TestCircuitBreakerStateTransitions(t *testing.T) { tests := []struct { name string - failures int - maxFailures int expectedStateBefore string expectedStateAfter string + failures int + maxFailures int }{ { name: "stays closed below threshold", @@ -543,8 +543,8 @@ func TestRetryExecutorNetworkErrors(t *testing.T) { }, nil) tests := []struct { - name string err error + name string shouldRetry bool }{ { @@ -1647,8 +1647,8 @@ func TestGracefulDegradationFullScenario(t *testing.T) { func TestIsTraefikDefaultCertError(t *testing.T) { tests := []struct { - name string err error + name string expected bool }{ { @@ -1680,8 +1680,8 @@ func TestIsTraefikDefaultCertError(t *testing.T) { func TestIsEOFError(t *testing.T) { tests := []struct { - name string err error + name string expected bool }{ { @@ -1723,8 +1723,8 @@ func TestIsEOFError(t *testing.T) { func TestIsCertificateError(t *testing.T) { tests := []struct { - name string err error + name string expected bool }{ { @@ -1811,8 +1811,8 @@ func TestRetryExecutorStartupErrors(t *testing.T) { _ = NewRetryExecutor(MetadataFetchRetryConfig(), nil) tests := []struct { - name string err error + name string shouldRetry bool }{ { @@ -1890,8 +1890,8 @@ func TestRetryExecutorIsRetryableErrorIntegration(t *testing.T) { re := NewRetryExecutor(DefaultRetryConfig(), nil) tests := []struct { - name string err error + name string shouldRetry bool }{ { @@ -1977,9 +1977,9 @@ func circuitBreakerStateToString(state CircuitBreakerState) string { } type mockNetError struct { + msg string timeout bool temporary bool - msg string } func (e *mockNetError) Error() string { return e.msg } diff --git a/goroutine_manager.go b/goroutine_manager.go index 853b8a7..25ef81a 100644 --- a/goroutine_manager.go +++ b/goroutine_manager.go @@ -10,16 +10,16 @@ import ( type GoroutineManager struct { ctx context.Context cancel context.CancelFunc - wg sync.WaitGroup - mu sync.RWMutex goroutines map[string]*managedGoroutine logger *Logger + wg sync.WaitGroup + mu sync.RWMutex } type managedGoroutine struct { - name string - cancel context.CancelFunc startTime time.Time + cancel context.CancelFunc + name string running bool } @@ -149,10 +149,10 @@ func (m *GoroutineManager) GetStatus() map[string]GoroutineStatus { // GoroutineStatus represents the status of a managed goroutine type GoroutineStatus struct { - Name string - Running bool StartTime time.Time + Name string Runtime time.Duration + Running bool } // ErrShutdownTimeout is returned when shutdown times out diff --git a/http_client_factory.go b/http_client_factory.go index 11d2dc9..9dcc758 100644 --- a/http_client_factory.go +++ b/http_client_factory.go @@ -12,30 +12,23 @@ import ( // HTTPClientConfig provides configuration for creating HTTP clients type HTTPClientConfig struct { - // Timeout for the entire request - Timeout time.Duration - // MaxRedirects allowed (0 means follow Go's default of 10) - MaxRedirects int - // UseCookieJar enables cookie jar for the client - UseCookieJar bool - // Connection settings + IdleConnTimeout time.Duration + MaxIdleConns int + ReadBufferSize int DialTimeout time.Duration KeepAlive time.Duration TLSHandshakeTimeout time.Duration ResponseHeaderTimeout time.Duration ExpectContinueTimeout time.Duration - IdleConnTimeout time.Duration - // Connection pool settings - MaxIdleConns int - MaxIdleConnsPerHost int - MaxConnsPerHost int - // Buffer settings - WriteBufferSize int - ReadBufferSize int - // Feature flags - ForceHTTP2 bool - DisableKeepAlives bool - DisableCompression bool + MaxRedirects int + MaxIdleConnsPerHost int + Timeout time.Duration + MaxConnsPerHost int + WriteBufferSize int + UseCookieJar bool + ForceHTTP2 bool + DisableKeepAlives bool + DisableCompression bool } // DefaultHTTPClientConfig returns the default configuration for general use diff --git a/http_client_factory_unit_test.go b/http_client_factory_unit_test.go index 0cdc15e..9a4c2fc 100644 --- a/http_client_factory_unit_test.go +++ b/http_client_factory_unit_test.go @@ -110,9 +110,9 @@ func TestHTTPClientFactoryValidateHTTPClientConfig(t *testing.T) { tests := []struct { name string + errorMsg string config HTTPClientConfig wantError bool - errorMsg string }{ { name: "valid config", diff --git a/http_client_pool.go b/http_client_pool.go index 8bdebc6..6352e3c 100644 --- a/http_client_pool.go +++ b/http_client_pool.go @@ -12,19 +12,19 @@ import ( // SharedTransportPool manages a pool of shared HTTP transports to prevent connection exhaustion type SharedTransportPool struct { - mu sync.RWMutex - transports map[string]*sharedTransport - maxConns int ctx context.Context + transports map[string]*sharedTransport cancel context.CancelFunc - clientCount int32 // SECURITY FIX: Track total HTTP clients - maxClients int32 // SECURITY FIX: Limit total clients to 5 + maxConns int + mu sync.RWMutex + clientCount int32 + maxClients int32 } type sharedTransport struct { + lastUsed time.Time transport *http.Transport refCount int - lastUsed time.Time } var ( diff --git a/input_validation_test.go b/input_validation_test.go index f1c3eb6..94e5e05 100644 --- a/input_validation_test.go +++ b/input_validation_test.go @@ -14,7 +14,7 @@ func TestInputValidator(t *testing.T) { } t.Run("Valid token validation", func(t *testing.T) { - validToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHs3UjpMC6M6FNqI2J-I2NxrragtnDxGxdJUvDERDQVHzeNlVQiuqWDEeO_O-0KptafbfyuGqfQxH_6dp2_MeFpAc" + validToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHs3UjpMC6M6FNqI2J-I2NxrragtnDxGxdJUvDERDQVHzeNlVQiuqWDEeO_O-0KptafbfyuGqfQxH_6dp2_MeFpAc" // trufflehog:ignore result := validator.ValidateToken(validToken) if !result.IsValid { @@ -428,12 +428,12 @@ func TestInputValidatorValidateToken(t *testing.T) { tests := []struct { name string token string - expectValid bool description string + expectValid bool }{ { name: "ValidJWTToken", - token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyLCJpYXQiOjE1MTYyMzkwMjJ9.signature", + token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyLCJpYXQiOjE1MTYyMzkwMjJ9.signature", // trufflehog:ignore expectValid: true, description: "Valid JWT token should pass validation", }, @@ -475,7 +475,7 @@ func TestInputValidatorValidateToken(t *testing.T) { }, { name: "MaliciousJWTWithExtraData", - token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig.malicious_extra", + token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig.malicious_extra", // trufflehog:ignore expectValid: false, description: "JWT with extra malicious data should fail validation", }, @@ -500,8 +500,8 @@ func TestInputValidatorValidateEmail(t *testing.T) { tests := []struct { name string email string - expectValid bool description string + expectValid bool }{ { name: "ValidEmail", @@ -578,8 +578,8 @@ func TestInputValidatorValidateURL(t *testing.T) { tests := []struct { name string url string - expectValid bool description string + expectValid bool }{ { name: "ValidHTTPSURL", @@ -669,8 +669,8 @@ func TestInputValidatorValidateClaim(t *testing.T) { name string claimName string claimValue string - expectValid bool description string + expectValid bool }{ { name: "ValidStringClaim", @@ -750,8 +750,8 @@ func TestInputValidatorValidateHeader(t *testing.T) { name string headerName string headerValue string - expectValid bool description string + expectValid bool }{ { name: "ValidHeader", @@ -830,8 +830,8 @@ func TestInputValidatorValidateUsername(t *testing.T) { tests := []struct { name string username string - expectValid bool description string + expectValid bool }{ { name: "ValidUsername", diff --git a/integration/integration_consolidated_test.go b/integration/integration_consolidated_test.go index 2096795..da11a49 100644 --- a/integration/integration_consolidated_test.go +++ b/integration/integration_consolidated_test.go @@ -726,20 +726,20 @@ type MockConfig struct { } type MockSession struct { - id string - userID string created time.Time lastUsed time.Time data map[string]interface{} + id string + userID string } type TestResult struct { - UserID int StartTime time.Time EndTime time.Time + Error error + UserID int Duration time.Duration Success bool - Error error } // ============================================================================ diff --git a/internal/cache/backends/config.go b/internal/cache/backends/config.go index a086eb0..6f60188 100644 --- a/internal/cache/backends/config.go +++ b/internal/cache/backends/config.go @@ -18,33 +18,22 @@ const ( // Config provides common configuration for cache backends type Config struct { - // Type specifies the backend type - Type BackendType - - // Memory backend settings - MaxSize int - MaxMemoryBytes int64 - CleanupInterval time.Duration - - // Redis backend settings - RedisAddr string - RedisPassword string - RedisDB int - RedisPrefix string - PoolSize int - - // Hybrid backend settings - L1Config *Config // Memory cache (L1) - L2Config *Config // Redis cache (L2) - AsyncWrites bool // Write to L2 asynchronously - - // Resilience settings + L2Config *Config + L1Config *Config + RedisPrefix string + Type BackendType + RedisAddr string + RedisPassword string + PoolSize int + RedisDB int + CleanupInterval time.Duration + MaxMemoryBytes int64 + MaxSize int + HealthCheckInterval time.Duration + AsyncWrites bool EnableCircuitBreaker bool EnableHealthCheck bool - HealthCheckInterval time.Duration - - // Metrics - EnableMetrics bool + EnableMetrics bool } // DefaultConfig returns a default configuration for in-memory caching diff --git a/internal/cache/backends/hybrid.go b/internal/cache/backends/hybrid.go index 008ecac..b8cf0e6 100644 --- a/internal/cache/backends/hybrid.go +++ b/internal/cache/backends/hybrid.go @@ -13,40 +13,30 @@ import ( // HybridBackend implements a two-tier cache with L1 (memory) and L2 (Redis) backends // It provides automatic failover, async writes for non-critical data, and optimized read paths type HybridBackend struct { - primary CacheBackend // L1: Memory cache for fast access - secondary CacheBackend // L2: Redis cache for distributed access - - // Configuration - syncWriteCacheTypes map[string]bool // Which cache types require synchronous writes + lastL2Error atomic.Value + secondary CacheBackend + primary CacheBackend + logger Logger + ctx context.Context + syncWriteCacheTypes map[string]bool asyncWriteBuffer chan *asyncWriteItem - - // Metrics - l1Hits atomic.Int64 - l2Hits atomic.Int64 - misses atomic.Int64 - l1Writes atomic.Int64 - l2Writes atomic.Int64 - errors atomic.Int64 - - // Fallback tracking - fallbackMode atomic.Bool // True when operating in degraded mode (L1 only) - lastL2Error atomic.Value // Stores last L2 error timestamp - - // Lifecycle - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - - // Logging - logger Logger + cancel context.CancelFunc + wg sync.WaitGroup + l1Hits atomic.Int64 + errors atomic.Int64 + l2Writes atomic.Int64 + l1Writes atomic.Int64 + misses atomic.Int64 + l2Hits atomic.Int64 + fallbackMode atomic.Bool } // asyncWriteItem represents an async write operation type asyncWriteItem struct { + ctx context.Context key string value []byte ttl time.Duration - ctx context.Context } // Logger interface for structured logging @@ -82,9 +72,9 @@ func (l *defaultLogger) Errorf(format string, args ...interface{}) { type HybridConfig struct { Primary CacheBackend Secondary CacheBackend - SyncWriteCacheTypes map[string]bool // Cache types requiring synchronous L2 writes - AsyncBufferSize int Logger Logger + SyncWriteCacheTypes map[string]bool + AsyncBufferSize int } // NewHybridBackend creates a new hybrid cache backend with L1 (memory) and L2 (Redis) tiers diff --git a/internal/cache/backends/hybrid_test.go b/internal/cache/backends/hybrid_test.go index 2f87473..4070683 100644 --- a/internal/cache/backends/hybrid_test.go +++ b/internal/cache/backends/hybrid_test.go @@ -17,23 +17,23 @@ import ( // mockBackend is a simple mock implementation of CacheBackend for testing type mockBackend struct { + pingError error data map[string]mockEntry + stats map[string]interface{} mu sync.RWMutex + getCalls atomic.Int32 + setCalls atomic.Int32 + deleteCalls atomic.Int32 failSet bool failGet bool failDelete bool failClear bool failPing bool - pingError error - stats map[string]interface{} - getCalls atomic.Int32 - setCalls atomic.Int32 - deleteCalls atomic.Int32 } type mockEntry struct { - value []byte expiresAt time.Time + value []byte } // mockBatchBackend extends mockBackend with batch operations diff --git a/internal/cache/backends/interface.go b/internal/cache/backends/interface.go index 65e455a..bc6c7d4 100644 --- a/internal/cache/backends/interface.go +++ b/internal/cache/backends/interface.go @@ -41,53 +41,22 @@ type CacheBackend interface { // BackendStats represents statistics for a cache backend type BackendStats struct { - // Type is the backend type - Type BackendType - - // Hits is the number of cache hits - Hits int64 - - // Misses is the number of cache misses - Misses int64 - - // Sets is the number of set operations - Sets int64 - - // Deletes is the number of delete operations - Deletes int64 - - // Errors is the number of errors - Errors int64 - - // Evictions is the number of evicted items - Evictions int64 - - // CurrentSize is the current number of items in cache - CurrentSize int64 - - // MaxSize is the maximum number of items (0 means unlimited) - MaxSize int64 - - // MemoryUsage is the approximate memory usage in bytes - MemoryUsage int64 - - // AverageGetLatency is the average latency for get operations + StartTime time.Time + LastErrorTime time.Time + Type BackendType + LastError string + Deletes int64 + Errors int64 + Evictions int64 + CurrentSize int64 + MaxSize int64 + MemoryUsage int64 AverageGetLatency time.Duration - - // AverageSetLatency is the average latency for set operations AverageSetLatency time.Duration - - // LastError is the last error encountered - LastError string - - // LastErrorTime is when the last error occurred - LastErrorTime time.Time - - // Uptime is how long the backend has been running - Uptime time.Duration - - // StartTime is when the backend was started - StartTime time.Time + Sets int64 + Misses int64 + Uptime time.Duration + Hits int64 } // BackendCapabilities describes the capabilities of a cache backend diff --git a/internal/cache/backends/memory.go b/internal/cache/backends/memory.go index 05e1e14..e66df44 100644 --- a/internal/cache/backends/memory.go +++ b/internal/cache/backends/memory.go @@ -11,14 +11,14 @@ import ( // memoryCacheItem represents an item in the memory cache type memoryCacheItem struct { - key string - value interface{} expiresAt time.Time createdAt time.Time accessedAt time.Time + value interface{} + element *list.Element + key string accessCount int64 size int64 - element *list.Element // for LRU tracking } // isExpired checks if the item is expired @@ -31,39 +31,31 @@ func (item *memoryCacheItem) isExpired() bool { // MemoryCacheBackend implements the CacheBackend interface using in-memory storage type MemoryCacheBackend struct { - mu sync.RWMutex - items map[string]*memoryCacheItem - lruList *list.List - maxSize int64 - maxMemory int64 - currentSize int64 - currentMemory int64 - - // Statistics - hits atomic.Int64 - misses atomic.Int64 - sets atomic.Int64 - deletes atomic.Int64 - evictions atomic.Int64 - errors atomic.Int64 - - // Latency tracking - totalGetTime atomic.Int64 - totalSetTime atomic.Int64 - getCount atomic.Int64 - setCount atomic.Int64 - - // Status - startTime time.Time - lastError string - lastErrorTime time.Time - cleanupTicker *time.Ticker - cleanupDone chan bool - closed atomic.Bool - - // Configuration + startTime time.Time + lastErrorTime time.Time + items map[string]*memoryCacheItem + lruList *list.List + cleanupDone chan bool + cleanupTicker *time.Ticker + evictionPolicy string + lastError string + currentMemory int64 + misses atomic.Int64 + deletes atomic.Int64 + evictions atomic.Int64 + errors atomic.Int64 + totalGetTime atomic.Int64 + totalSetTime atomic.Int64 + getCount atomic.Int64 + setCount atomic.Int64 + sets atomic.Int64 + hits atomic.Int64 + maxSize int64 + currentSize int64 + maxMemory int64 cleanupInterval time.Duration - evictionPolicy string // "lru", "lfu", "fifo" + mu sync.RWMutex + closed atomic.Bool } // NewMemoryCacheBackend creates a new memory cache backend diff --git a/internal/cache/backends/redis_health.go b/internal/cache/backends/redis_health.go index fd6a908..f135477 100644 --- a/internal/cache/backends/redis_health.go +++ b/internal/cache/backends/redis_health.go @@ -9,30 +9,24 @@ import ( // HealthMonitor continuously monitors Redis connection health and triggers reconnections type HealthMonitor struct { - pool *ConnectionPool - config *HealthMonitorConfig - - // State - healthy atomic.Bool - running atomic.Bool - lastCheckTime atomic.Int64 // Unix timestamp - - // Metrics + pool *ConnectionPool + config *HealthMonitorConfig + stopChan chan struct{} + wg sync.WaitGroup + lastCheckTime atomic.Int64 consecutiveFailures atomic.Int64 totalChecks atomic.Int64 totalFailures atomic.Int64 - - // Lifecycle - stopChan chan struct{} - wg sync.WaitGroup + healthy atomic.Bool + running atomic.Bool } // HealthMonitorConfig configures the health monitor type HealthMonitorConfig struct { - CheckInterval time.Duration // How often to check health - Timeout time.Duration // Timeout for health check - UnhealthyThreshold int // Consecutive failures before marking unhealthy OnHealthChange func(healthy bool) + CheckInterval time.Duration + Timeout time.Duration + UnhealthyThreshold int } // DefaultHealthMonitorConfig returns default health monitor configuration diff --git a/internal/cache/backends/resp_test.go b/internal/cache/backends/resp_test.go index c60c709..3b4ae6b 100644 --- a/internal/cache/backends/resp_test.go +++ b/internal/cache/backends/resp_test.go @@ -15,8 +15,8 @@ import ( func TestRESPWriter_WriteCommand(t *testing.T) { tests := []struct { name string - args []string expected string + args []string }{ { name: "Simple command", @@ -205,9 +205,9 @@ func TestRESPReader_ReadInteger(t *testing.T) { // TestRESPReader_ReadBulkString tests reading bulk strings func TestRESPReader_ReadBulkString(t *testing.T) { tests := []struct { + expected interface{} name string input string - expected interface{} wantErr bool isNil bool }{ @@ -440,10 +440,10 @@ func TestRESPHelpers(t *testing.T) { // TestRESPRoundTrip tests full round-trip encoding/decoding func TestRESPRoundTrip(t *testing.T) { tests := []struct { - name string - command []string - response string expected interface{} + name string + response string + command []string }{ { name: "PING command", diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 6df6a9d..7964684 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -33,21 +33,19 @@ type Logger interface { // Config provides configuration for the cache type Config struct { + Logger Logger + JWKConfig *JWKConfig + MetadataConfig *MetadataConfig + TokenConfig *TokenConfig Type Type - MaxSize int - MaxMemoryBytes int64 DefaultTTL time.Duration CleanupInterval time.Duration - EnableCompression bool + MaxMemoryBytes int64 + MaxSize int EnableMetrics bool EnableAutoCleanup bool EnableMemoryLimit bool - Logger Logger - - // Type-specific configurations - TokenConfig *TokenConfig - MetadataConfig *MetadataConfig - JWKConfig *JWKConfig + EnableCompression bool } // TokenConfig provides token-specific cache configuration @@ -59,11 +57,11 @@ type TokenConfig struct { // MetadataConfig provides metadata-specific cache configuration type MetadataConfig struct { + SecurityCriticalFields []string GracePeriod time.Duration ExtendedGracePeriod time.Duration MaxGracePeriod time.Duration SecurityCriticalMaxGracePeriod time.Duration - SecurityCriticalFields []string } // JWKConfig provides JWK-specific cache configuration @@ -75,45 +73,35 @@ type JWKConfig struct { // Item represents a single cache entry type Item struct { - Key string - Value interface{} - Size int64 ExpiresAt time.Time LastAccessed time.Time - AccessCount int64 + Value interface{} + Metadata map[string]interface{} + element *list.Element + Key string CacheType Type - - // Type-specific metadata - Metadata map[string]interface{} - - // LRU list element reference - element *list.Element + Size int64 + AccessCount int64 } // Cache provides a single, unified cache implementation type Cache struct { - mu sync.RWMutex - items map[string]*Item - lruList *list.List - config Config - logger Logger - - // Memory management + config Config + ctx context.Context + logger Logger + cancel context.CancelFunc + lruList *list.List + items map[string]*Item + stopCleanup chan bool + wg sync.WaitGroup currentSize int64 currentMemory int64 - - // Metrics - hits int64 - misses int64 - evictions int64 - sets int64 - - // Lifecycle management - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - stopCleanup chan bool - closed int32 + hits int64 + misses int64 + evictions int64 + sets int64 + mu sync.RWMutex + closed int32 } // DefaultConfig returns a default cache configuration diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index 5b2565a..7a66b5e 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -1750,19 +1750,19 @@ func TestAdvancedEdgeCases(t *testing.T) { // Test with various data types testCases := []struct { - key string value interface{} + key string }{ - {"string", "test string"}, - {"int", 42}, - {"float", 3.14159}, - {"bool", true}, - {"slice", []string{"a", "b", "c"}}, - {"map", map[string]int{"one": 1, "two": 2}}, - {"nil", nil}, - {"empty-string", ""}, - {"empty-slice", []string{}}, - {"empty-map", map[string]interface{}{}}, + {key: "string", value: "test string"}, + {key: "int", value: 42}, + {key: "float", value: 3.14159}, + {key: "bool", value: true}, + {key: "slice", value: []string{"a", "b", "c"}}, + {key: "map", value: map[string]int{"one": 1, "two": 2}}, + {key: "nil", value: nil}, + {key: "empty-string", value: ""}, + {key: "empty-slice", value: []string{}}, + {key: "empty-map", value: map[string]interface{}{}}, } for _, tc := range testCases { diff --git a/internal/cache/manager.go b/internal/cache/manager.go index 6c19b42..af2a379 100644 --- a/internal/cache/manager.go +++ b/internal/cache/manager.go @@ -7,22 +7,17 @@ import ( // Manager manages multiple cache instances with singleton pattern type Manager struct { - mu sync.RWMutex - - // Core caches + logger Logger tokenCache *Cache metadataCache *Cache jwkCache *Cache sessionCache *Cache generalCache *Cache - - // Typed wrappers typedToken *TokenCache typedMetadata *MetadataCache typedJWK *JWKCache typedSession *SessionCache - - logger Logger + mu sync.RWMutex } var ( diff --git a/internal/cache/resilience/circuit_breaker.go b/internal/cache/resilience/circuit_breaker.go index 3a2c0e5..f507429 100644 --- a/internal/cache/resilience/circuit_breaker.go +++ b/internal/cache/resilience/circuit_breaker.go @@ -48,23 +48,12 @@ func (s State) String() string { // CircuitBreakerConfig holds configuration for the circuit breaker type CircuitBreakerConfig struct { - // MaxFailures is the number of consecutive failures before opening the circuit - MaxFailures int - - // FailureThreshold is the failure rate threshold (0.0 to 1.0) - FailureThreshold float64 - - // Timeout is how long the circuit stays open before trying half-open - Timeout time.Duration - - // HalfOpenMaxRequests is the number of requests allowed in half-open state + OnStateChange func(from, to State) + MaxFailures int + FailureThreshold float64 + Timeout time.Duration HalfOpenMaxRequests int - - // ResetTimeout is how long to wait before resetting counters in closed state - ResetTimeout time.Duration - - // OnStateChange is called when the circuit breaker changes state - OnStateChange func(from, to State) + ResetTimeout time.Duration } // DefaultCircuitBreakerConfig returns default configuration @@ -80,28 +69,20 @@ func DefaultCircuitBreakerConfig() *CircuitBreakerConfig { // CircuitBreaker implements the circuit breaker pattern type CircuitBreaker struct { - config *CircuitBreakerConfig - - // State management - state atomic.Int32 - lastStateChange time.Time - stateMu sync.RWMutex - - // Failure tracking - consecutiveFailures atomic.Int32 - totalRequests atomic.Int64 + nextRetryTime time.Time + lastStateChange time.Time + lastSuccessTime time.Time + lastFailureTime time.Time + config *CircuitBreakerConfig totalFailures atomic.Int64 + totalRequests atomic.Int64 + stateTransitions atomic.Int64 + rejectedRequests atomic.Int64 + stateMu sync.RWMutex + timeMu sync.RWMutex halfOpenRequests atomic.Int32 - - // Timing - lastFailureTime time.Time - lastSuccessTime time.Time - nextRetryTime time.Time - timeMu sync.RWMutex - - // Metrics - stateTransitions atomic.Int64 - rejectedRequests atomic.Int64 + consecutiveFailures atomic.Int32 + state atomic.Int32 } // NewCircuitBreaker creates a new circuit breaker @@ -313,17 +294,17 @@ func (cb *CircuitBreaker) Stats() CircuitBreakerStats { // CircuitBreakerStats holds statistics for the circuit breaker type CircuitBreakerStats struct { - State State - ConsecutiveFailures int32 + LastFailureTime time.Time + LastSuccessTime time.Time + LastStateChange time.Time + NextRetryTime time.Time TotalRequests int64 TotalFailures int64 SuccessRate float64 RejectedRequests int64 StateTransitions int64 - LastFailureTime time.Time - LastSuccessTime time.Time - LastStateChange time.Time - NextRetryTime time.Time + State State + ConsecutiveFailures int32 } // IsHealthy returns true if the circuit breaker is in a healthy state diff --git a/internal/cache/resilience/circuit_breaker_backend_test.go b/internal/cache/resilience/circuit_breaker_backend_test.go index 67901be..1340ee6 100644 --- a/internal/cache/resilience/circuit_breaker_backend_test.go +++ b/internal/cache/resilience/circuit_breaker_backend_test.go @@ -28,8 +28,8 @@ type mockBackend struct { } type mockEntry struct { - value []byte expiresAt time.Time + value []byte } func newMockBackend() *mockBackend { diff --git a/internal/cache/resilience/health_check.go b/internal/cache/resilience/health_check.go index 1e99db4..c5abd7b 100644 --- a/internal/cache/resilience/health_check.go +++ b/internal/cache/resilience/health_check.go @@ -41,26 +41,13 @@ func (h HealthStatus) String() string { // HealthCheckConfig holds configuration for the health checker type HealthCheckConfig struct { - // CheckInterval is how often to check health - CheckInterval time.Duration - - // Timeout is the timeout for each health check - Timeout time.Duration - - // HealthyThreshold is the number of consecutive successes to become healthy - HealthyThreshold int - - // UnhealthyThreshold is the number of consecutive failures to become unhealthy + OnStatusChange func(from, to HealthStatus) + CheckFunc func(ctx context.Context) error + CheckInterval time.Duration + Timeout time.Duration + HealthyThreshold int UnhealthyThreshold int - - // DegradedThreshold is the latency threshold in ms to mark as degraded - DegradedThreshold time.Duration - - // OnStatusChange is called when health status changes - OnStatusChange func(from, to HealthStatus) - - // CheckFunc is the function to check health - CheckFunc func(ctx context.Context) error + DegradedThreshold time.Duration } // DefaultHealthCheckConfig returns default configuration @@ -76,31 +63,23 @@ func DefaultHealthCheckConfig() *HealthCheckConfig { // HealthChecker monitors the health of a backend type HealthChecker struct { - config *HealthCheckConfig - - // Status tracking - status atomic.Int32 - consecutiveSuccesses atomic.Int32 + lastCheckTime time.Time + lastSuccessTime time.Time + lastFailureTime time.Time + config *HealthCheckConfig + stopChan chan struct{} + ticker *time.Ticker + wg sync.WaitGroup + statusChanges atomic.Int64 + totalChecks atomic.Int64 + totalSuccesses atomic.Int64 + totalFailures atomic.Int64 + averageLatency atomic.Int64 + timeMu sync.RWMutex consecutiveFailures atomic.Int32 - - // Timing - lastCheckTime time.Time - lastSuccessTime time.Time - lastFailureTime time.Time - averageLatency atomic.Int64 - timeMu sync.RWMutex - - // Metrics - totalChecks atomic.Int64 - totalSuccesses atomic.Int64 - totalFailures atomic.Int64 - statusChanges atomic.Int64 - - // Lifecycle - ticker *time.Ticker - stopChan chan struct{} - stopped atomic.Bool - wg sync.WaitGroup + consecutiveSuccesses atomic.Int32 + stopped atomic.Bool + status atomic.Int32 } // NewHealthChecker creates a new health checker @@ -342,19 +321,19 @@ func (hc *HealthChecker) Stats() HealthCheckerStats { // HealthCheckerStats holds statistics for the health checker type HealthCheckerStats struct { - Status HealthStatus - ConsecutiveSuccesses int32 - ConsecutiveFailures int32 + LastCheckTime time.Time + LastFailureTime time.Time + LastSuccessTime time.Time TotalChecks int64 TotalSuccesses int64 TotalFailures int64 SuccessRate float64 AverageLatency time.Duration StatusChanges int64 - LastCheckTime time.Time - LastSuccessTime time.Time - LastFailureTime time.Time HealthScore float64 + Status HealthStatus + ConsecutiveFailures int32 + ConsecutiveSuccesses int32 } // Reset resets the health checker statistics diff --git a/internal/cache/resilience/health_check_backend.go b/internal/cache/resilience/health_check_backend.go index 6506aad..0b40d13 100644 --- a/internal/cache/resilience/health_check_backend.go +++ b/internal/cache/resilience/health_check_backend.go @@ -12,20 +12,16 @@ import ( // HealthCheckBackend wraps a cache backend with health checking type HealthCheckBackend struct { - backend backends.CacheBackend - config *HealthCheckConfig - - // Health tracking + lastCheck time.Time + backend backends.CacheBackend + ctx context.Context + config *HealthCheckConfig + cancel context.CancelFunc + wg sync.WaitGroup + checkMutex sync.RWMutex status atomic.Int32 consecutiveFails atomic.Int32 consecutiveOK atomic.Int32 - lastCheck time.Time - checkMutex sync.RWMutex - - // Lifecycle - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup } // NewHealthCheckBackend creates a new health check wrapped backend diff --git a/internal/cache/typed_cache.go b/internal/cache/typed_cache.go index 1e2de29..bc50f05 100644 --- a/internal/cache/typed_cache.go +++ b/internal/cache/typed_cache.go @@ -292,12 +292,12 @@ type SessionCache struct { // SessionData represents session information type SessionData struct { + ExpiresAt time.Time `json:"expires_at"` + Claims map[string]interface{} `json:"claims"` ID string `json:"id"` UserID string `json:"user_id"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` - ExpiresAt time.Time `json:"expires_at"` - Claims map[string]interface{} `json:"claims"` } // NewSessionCache creates a new session cache diff --git a/internal/cleanup/cleanup_test.go b/internal/cleanup/cleanup_test.go index 7a30aff..1ae4046 100644 --- a/internal/cleanup/cleanup_test.go +++ b/internal/cleanup/cleanup_test.go @@ -11,10 +11,10 @@ import ( // Mock logger for testing type mockLogger struct { - mu sync.Mutex logs []string errLogs []string debugLog []string + mu sync.Mutex } func (m *mockLogger) Logf(format string, args ...interface{}) { diff --git a/internal/cleanup/manager.go b/internal/cleanup/manager.go index bae7a9d..5b31f86 100644 --- a/internal/cleanup/manager.go +++ b/internal/cleanup/manager.go @@ -19,20 +19,20 @@ type Logger interface { // BackgroundTask represents a recurring background task type BackgroundTask struct { - name string - interval time.Duration - taskFunc func() + lastRun time.Time + logger Logger + ctx context.Context ticker *time.Ticker stopChan chan bool - isRunning int32 - logger Logger waitGroup *sync.WaitGroup - lastRun time.Time + taskFunc func() + cancelFunc context.CancelFunc + name string runCount int64 errorCount int64 + interval time.Duration mu sync.RWMutex - ctx context.Context - cancelFunc context.CancelFunc + isRunning int32 } // NewBackgroundTask creates a new background task @@ -183,11 +183,11 @@ func (bt *BackgroundTask) IsRunning() bool { // TaskRegistry manages all background tasks type TaskRegistry struct { - tasks map[string]*BackgroundTask - mu sync.RWMutex logger Logger - maxTasks int + tasks map[string]*BackgroundTask circuitBreaker *TaskCircuitBreaker + maxTasks int + mu sync.RWMutex } // globalTaskRegistry is the singleton task registry diff --git a/internal/cleanup/workers.go b/internal/cleanup/workers.go index c497d96..6f2da12 100644 --- a/internal/cleanup/workers.go +++ b/internal/cleanup/workers.go @@ -11,14 +11,14 @@ import ( // TaskCircuitBreaker prevents task creation failures from cascading type TaskCircuitBreaker struct { + lastFailureTime time.Time + logger Logger + taskFailures map[string]int32 + timeout time.Duration + mu sync.RWMutex failureThreshold int32 failureCount int32 - lastFailureTime time.Time - timeout time.Duration - state int32 // 0: closed, 1: open - logger Logger - mu sync.RWMutex - taskFailures map[string]int32 + state int32 } // CircuitBreakerState represents the state of the circuit breaker @@ -140,14 +140,14 @@ func (cb *TaskCircuitBreaker) GetState() CircuitBreakerState { // TaskMemoryMonitor monitors memory usage and can trigger cleanup type TaskMemoryMonitor struct { + lastCheck time.Time logger Logger registry *TaskRegistry + stopChan chan bool memoryThreshold uint64 checkInterval time.Duration - isMonitoring int32 - stopChan chan bool - lastCheck time.Time mu sync.RWMutex + isMonitoring int32 } var ( @@ -310,13 +310,13 @@ func (tmm *TaskMemoryMonitor) GetStats() map[string]interface{} { // WorkerPool manages a pool of worker goroutines for task execution type WorkerPool struct { - workers int - taskQueue chan func() - workerWg sync.WaitGroup - isRunning int32 logger Logger + taskQueue chan func() stopChan chan bool metrics WorkerPoolMetrics + workerWg sync.WaitGroup + workers int + isRunning int32 } // WorkerPoolMetrics tracks worker pool performance diff --git a/internal/features/flags.go b/internal/features/flags.go index eac8f98..15b81c2 100644 --- a/internal/features/flags.go +++ b/internal/features/flags.go @@ -12,9 +12,9 @@ import ( type FeatureFlag struct { name string description string - enabled atomic.Bool - mu sync.RWMutex callbacks []func(bool) + mu sync.RWMutex + enabled atomic.Bool } // FeatureManager manages all feature flags in the application diff --git a/internal/pool/transport.go b/internal/pool/transport.go index 47a91b3..d3ec823 100644 --- a/internal/pool/transport.go +++ b/internal/pool/transport.go @@ -14,50 +14,41 @@ import ( // and resource leaks. It provides centralized management of HTTP client transports with // proper lifecycle management and security controls. type TransportPool struct { - mu sync.RWMutex - transports map[string]*sharedTransport - maxConns int ctx context.Context + transports map[string]*sharedTransport cancel context.CancelFunc - clientCount int32 // Track total HTTP clients - maxClients int32 // Limit total clients + maxConns int + mu sync.RWMutex + clientCount int32 + maxClients int32 } // sharedTransport wraps an HTTP transport with reference counting type sharedTransport struct { - transport *http.Transport - refCount int32 lastUsed time.Time + transport *http.Transport config TransportConfig + refCount int32 } // TransportConfig defines configuration for HTTP transports type TransportConfig struct { - // Timeouts - DialTimeout time.Duration - TLSHandshakeTimeout time.Duration + MaxConnsPerHost int + WriteBufferSize int ResponseHeaderTimeout time.Duration ExpectContinueTimeout time.Duration IdleConnTimeout time.Duration KeepAlive time.Duration - - // Connection limits - MaxIdleConns int - MaxIdleConnsPerHost int - MaxConnsPerHost int - - // Features - ForceHTTP2 bool - DisableKeepAlives bool - DisableCompression bool - - // Buffer sizes - WriteBufferSize int - ReadBufferSize int - - // TLS - InsecureSkipVerify bool - MinTLSVersion uint16 + TLSHandshakeTimeout time.Duration + MaxIdleConns int + DialTimeout time.Duration + MaxIdleConnsPerHost int + ReadBufferSize int + MinTLSVersion uint16 + ForceHTTP2 bool + DisableCompression bool + InsecureSkipVerify bool + DisableKeepAlives bool } var ( diff --git a/internal/providers/azure_test.go b/internal/providers/azure_test.go index 01af64b..bcda575 100644 --- a/internal/providers/azure_test.go +++ b/internal/providers/azure_test.go @@ -154,10 +154,10 @@ func TestAzureProvider_ValidateTokens(t *testing.T) { provider := NewAzureProvider() tests := []struct { - name string - session *mockSession verifierError error + session *mockSession cacheData map[string]interface{} + name string expectedResult ValidationResult }{ { @@ -369,9 +369,9 @@ func TestAzureProvider_OfflineAccessHandling(t *testing.T) { tests := []struct { name string - inputScopes []string - expectedCount int // Expected number of offline_access scopes (should be 1) description string + inputScopes []string + expectedCount int }{ { name: "No offline_access - should add one", diff --git a/internal/providers/base_test.go b/internal/providers/base_test.go index 3c30f6c..188df74 100644 --- a/internal/providers/base_test.go +++ b/internal/providers/base_test.go @@ -8,10 +8,10 @@ import ( // Mock implementations for testing type mockSession struct { - authenticated bool idToken string accessToken string refreshToken string + authenticated bool } func (s *mockSession) GetIDToken() string { return s.idToken } @@ -338,10 +338,10 @@ func TestBaseProvider_ValidateTokenExpiry(t *testing.T) { gracePeriod := 5 * time.Minute tests := []struct { - name string claims map[string]interface{} - cacheFound bool + name string expectedResult ValidationResult + cacheFound bool }{ { name: "Token not found in cache, has refresh token", @@ -438,10 +438,10 @@ func TestBaseProvider_ValidateTokenExpiry_NoRefreshToken(t *testing.T) { gracePeriod := 5 * time.Minute tests := []struct { - name string claims map[string]interface{} - cacheFound bool + name string expectedResult ValidationResult + cacheFound bool }{ { name: "Token not found in cache, no refresh token", diff --git a/internal/providers/factory_test.go b/internal/providers/factory_test.go index beb94a7..3dee5c4 100644 --- a/internal/providers/factory_test.go +++ b/internal/providers/factory_test.go @@ -25,9 +25,9 @@ func TestProviderFactory_CreateProvider(t *testing.T) { tests := []struct { name string issuerURL string + errMsg string expectedType ProviderType wantErr bool - errMsg string }{ { name: "Google provider", @@ -158,10 +158,10 @@ func TestProviderFactory_CreateProviderByType(t *testing.T) { tests := []struct { name string + errMsg string providerType ProviderType expectedType ProviderType wantErr bool - errMsg string }{ { name: "Generic provider", diff --git a/internal/providers/generic_test.go b/internal/providers/generic_test.go index 7fcda35..c3d0560 100644 --- a/internal/providers/generic_test.go +++ b/internal/providers/generic_test.go @@ -136,9 +136,9 @@ func TestGenericProvider_ValidateTokens(t *testing.T) { provider := NewGenericProvider() tests := []struct { - name string - session *mockSession verifierError error + session *mockSession + name string expectedResult ValidationResult }{ { diff --git a/internal/providers/google_test.go b/internal/providers/google_test.go index ef2f98e..e8a5967 100644 --- a/internal/providers/google_test.go +++ b/internal/providers/google_test.go @@ -172,8 +172,8 @@ func TestGoogleProvider_OfflineAccessFiltering(t *testing.T) { tests := []struct { name string - inputScopes []string description string + inputScopes []string }{ { name: "Multiple offline_access occurrences", diff --git a/internal/providers/registry_test.go b/internal/providers/registry_test.go index ab67f74..06fcb0d 100644 --- a/internal/providers/registry_test.go +++ b/internal/providers/registry_test.go @@ -82,9 +82,9 @@ func TestProviderRegistry_GetProviderByType(t *testing.T) { registry.RegisterProvider(googleProvider) tests := []struct { + expected OIDCProvider name string providerType ProviderType - expected OIDCProvider }{ { name: "Get Generic provider", @@ -180,9 +180,9 @@ func TestProviderRegistry_DetectProvider(t *testing.T) { registry.RegisterProvider(gitlabProvider) tests := []struct { + expected OIDCProvider name string issuerURL string - expected OIDCProvider }{ { name: "Google provider detection", @@ -640,9 +640,9 @@ func TestProviderRegistry_GitLabDetection_RealWorldURLs(t *testing.T) { registry.RegisterProvider(githubProvider) realWorldTests := []struct { + expected OIDCProvider name string issuerURL string - expected OIDCProvider }{ // Actual self-hosted GitLab examples from issue #61 { diff --git a/internal/providers/validation_test.go b/internal/providers/validation_test.go index a4290f9..50de80f 100644 --- a/internal/providers/validation_test.go +++ b/internal/providers/validation_test.go @@ -20,8 +20,8 @@ func TestValidateIssuerURL(t *testing.T) { tests := []struct { name string issuerURL string - wantErr bool errMsg string + wantErr bool }{ { name: "valid https URL", @@ -106,8 +106,8 @@ func TestValidateClientID(t *testing.T) { tests := []struct { name string clientID string - wantErr bool errMsg string + wantErr bool }{ { name: "valid client ID", @@ -173,9 +173,9 @@ func TestValidateClientID(t *testing.T) { func TestValidateScopes(t *testing.T) { tests := []struct { name string + errMsg string scopes []string wantErr bool - errMsg string }{ { name: "valid scopes with openid", @@ -248,8 +248,8 @@ func TestValidateRedirectURL(t *testing.T) { tests := []struct { name string redirectURL string - wantErr bool errMsg string + wantErr bool }{ { name: "valid https redirect URL", @@ -315,11 +315,11 @@ func TestValidateRedirectURL(t *testing.T) { // TestValidateProviderSpecificConfig tests provider-specific configuration validation func TestValidateProviderSpecificConfig(t *testing.T) { tests := []struct { - name string provider OIDCProvider config map[string]interface{} - wantErr bool + name string errMsg string + wantErr bool }{ { name: "valid Google config", @@ -458,8 +458,8 @@ func TestValidateGoogleConfig_EdgeCases(t *testing.T) { googleProvider := NewGoogleProvider() tests := []struct { - name string config map[string]interface{} + name string wantErr bool }{ { @@ -502,10 +502,10 @@ func TestValidateAzureConfig_EdgeCases(t *testing.T) { azureProvider := NewAzureProvider() tests := []struct { - name string config map[string]interface{} - wantErr bool + name string errMsg string + wantErr bool }{ { name: "valid tenant ID format", diff --git a/internal/providers/warnings.go b/internal/providers/warnings.go index 2b57268..12f7318 100644 --- a/internal/providers/warnings.go +++ b/internal/providers/warnings.go @@ -7,9 +7,9 @@ import ( // ProviderWarning represents a warning about provider limitations or requirements. type ProviderWarning struct { - ProviderType ProviderType - Level string // "info", "warning", "error" + Level string Message string + ProviderType ProviderType } // GetProviderWarnings returns warnings about provider-specific limitations. diff --git a/internal/providers/warnings_test.go b/internal/providers/warnings_test.go index 926c593..3fd7040 100644 --- a/internal/providers/warnings_test.go +++ b/internal/providers/warnings_test.go @@ -9,9 +9,9 @@ import ( func TestGetProviderWarnings(t *testing.T) { tests := []struct { name string + checkContent string providerType ProviderType expectCount int - checkContent string }{ { name: "GitHub has OAuth 2.0 warning", diff --git a/internal/recovery/base.go b/internal/recovery/base.go index 19dc12f..287b989 100644 --- a/internal/recovery/base.go +++ b/internal/recovery/base.go @@ -34,21 +34,15 @@ type Logger interface { // for all recovery mechanism implementations. It handles request counting, // success/failure tracking, and timestamp management in a thread-safe manner. type BaseRecoveryMechanism struct { - // name identifies the recovery mechanism instance - name string - // logger provides structured logging capabilities - logger Logger - - // Metrics tracked with atomic operations for thread safety + logger Logger + name string + lastSuccessStr string + lastFailureStr string totalRequests int64 successCount int64 failureCount int64 - lastSuccessStr string - lastFailureStr string - - // mutexes for thread-safe timestamp updates - successMutex sync.RWMutex - failureMutex sync.RWMutex + successMutex sync.RWMutex + failureMutex sync.RWMutex } // NewBaseRecoveryMechanism creates a new base recovery mechanism with the given name and logger. @@ -182,10 +176,10 @@ const ( // HTTPError represents an HTTP error with status code and message type HTTPError struct { - StatusCode int + Headers map[string]string Message string Body []byte - Headers map[string]string + StatusCode int } // Error implements the error interface diff --git a/internal/recovery/circuit_breaker.go b/internal/recovery/circuit_breaker.go index c783179..5f780af 100644 --- a/internal/recovery/circuit_breaker.go +++ b/internal/recovery/circuit_breaker.go @@ -60,20 +60,14 @@ func DefaultCircuitBreakerConfig() CircuitBreakerConfig { // CircuitBreaker implements the circuit breaker pattern for fault tolerance. // It prevents cascading failures by temporarily blocking requests to a failing service. type CircuitBreaker struct { - *BaseRecoveryMechanism - config CircuitBreakerConfig - - // State management - state int32 // atomic: CircuitBreakerState lastStateChange time.Time - stateMutex sync.RWMutex - - // Failure tracking - consecutiveFailures int32 // atomic - consecutiveSuccesses int32 // atomic - - // Half-open state management - halfOpenRequests int32 // atomic + *BaseRecoveryMechanism + config CircuitBreakerConfig + stateMutex sync.RWMutex + state int32 + consecutiveFailures int32 + consecutiveSuccesses int32 + halfOpenRequests int32 } // NewCircuitBreaker creates a new circuit breaker with the given configuration diff --git a/internal/recovery/metrics.go b/internal/recovery/metrics.go index 1f207f9..4487333 100644 --- a/internal/recovery/metrics.go +++ b/internal/recovery/metrics.go @@ -15,20 +15,13 @@ import ( // RetryConfig defines configuration for the retry executor type RetryConfig struct { - // MaxAttempts is the maximum number of retry attempts - MaxAttempts int - // InitialDelay is the initial delay between retries - InitialDelay time.Duration - // MaxDelay is the maximum delay between retries - MaxDelay time.Duration - // Multiplier is the backoff multiplier - Multiplier float64 - // RandomizationFactor adds jitter to delays (0.0 to 1.0) - RandomizationFactor float64 - // RetryableErrors defines which errors should trigger a retry - RetryableErrors []string - // RetryableStatusCodes defines which HTTP status codes should trigger a retry + RetryableErrors []string RetryableStatusCodes []int + MaxAttempts int + InitialDelay time.Duration + MaxDelay time.Duration + Multiplier float64 + RandomizationFactor float64 } // DefaultRetryConfig returns sensible default retry configuration @@ -46,13 +39,11 @@ func DefaultRetryConfig() RetryConfig { // RetryExecutor implements retry logic with exponential backoff type RetryExecutor struct { + lastRetryTime time.Time *BaseRecoveryMechanism - config RetryConfig - - // Metrics + config RetryConfig totalRetries int64 maxRetriesHit int64 - lastRetryTime time.Time retryTimeMutex sync.RWMutex } diff --git a/internal/recovery/metrics_test.go b/internal/recovery/metrics_test.go index 24050ee..cc647f2 100644 --- a/internal/recovery/metrics_test.go +++ b/internal/recovery/metrics_test.go @@ -273,17 +273,17 @@ func TestRetryExecutor_isRetryableError(t *testing.T) { executor := NewRetryExecutor(config, logger) tests := []struct { - name string err error + name string expected bool }{ - {"nil error", nil, false}, - {"connection refused", errors.New("connection refused"), true}, - {"timeout", errors.New("TIMEOUT"), true}, // case insensitive - {"EOF", errors.New("EOF"), false}, - {"random error", errors.New("something else"), false}, - {"context cancelled", context.Canceled, false}, - {"context deadline exceeded", context.DeadlineExceeded, false}, + {name: "nil error", err: nil, expected: false}, + {name: "connection refused", err: errors.New("connection refused"), expected: true}, + {name: "timeout", err: errors.New("TIMEOUT"), expected: true}, // case insensitive + {name: "EOF", err: errors.New("EOF"), expected: false}, + {name: "random error", err: errors.New("something else"), expected: false}, + {name: "context cancelled", err: context.Canceled, expected: false}, + {name: "context deadline exceeded", err: context.DeadlineExceeded, expected: false}, } for _, tt := range tests { diff --git a/internal/recovery/recovery_test.go b/internal/recovery/recovery_test.go index 1632739..95f6f86 100644 --- a/internal/recovery/recovery_test.go +++ b/internal/recovery/recovery_test.go @@ -13,10 +13,10 @@ import ( // Mock logger for testing type mockLogger struct { - mu sync.Mutex logs []string errLogs []string debugLog []string + mu sync.Mutex } func (m *mockLogger) Logf(format string, args ...interface{}) { @@ -202,13 +202,13 @@ func TestBaseRecoveryMechanism_ConcurrentAccess(t *testing.T) { // CircuitBreakerState tests func TestCircuitBreakerState_String(t *testing.T) { tests := []struct { - state CircuitBreakerState expected string + state CircuitBreakerState }{ - {CircuitBreakerClosed, "closed"}, - {CircuitBreakerOpen, "open"}, - {CircuitBreakerHalfOpen, "half-open"}, - {CircuitBreakerState(99), "unknown"}, + {state: CircuitBreakerClosed, expected: "closed"}, + {state: CircuitBreakerOpen, expected: "open"}, + {state: CircuitBreakerHalfOpen, expected: "half-open"}, + {state: CircuitBreakerState(99), expected: "unknown"}, } for _, tt := range tests { diff --git a/internal/testutil/mocks/interfaces.go b/internal/testutil/mocks/interfaces.go index e0b2a47..7f00784 100644 --- a/internal/testutil/mocks/interfaces.go +++ b/internal/testutil/mocks/interfaces.go @@ -29,26 +29,26 @@ type JWK struct { type TokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token,omitempty"` IDToken string `json:"id_token,omitempty"` Scope string `json:"scope,omitempty"` + ExpiresIn int `json:"expires_in"` } // IntrospectionResponse represents a token introspection response type IntrospectionResponse struct { - Active bool `json:"active"` Scope string `json:"scope,omitempty"` ClientID string `json:"client_id,omitempty"` Username string `json:"username,omitempty"` TokenType string `json:"token_type,omitempty"` - Exp int64 `json:"exp,omitempty"` - Iat int64 `json:"iat,omitempty"` - Nbf int64 `json:"nbf,omitempty"` Sub string `json:"sub,omitempty"` Aud string `json:"aud,omitempty"` Iss string `json:"iss,omitempty"` Jti string `json:"jti,omitempty"` + Exp int64 `json:"exp,omitempty"` + Iat int64 `json:"iat,omitempty"` + Nbf int64 `json:"nbf,omitempty"` + Active bool `json:"active"` } // JWKCache is a testify mock for JWK caching operations diff --git a/internal/testutil/mocks/session.go b/internal/testutil/mocks/session.go index 2ccc4ba..697f469 100644 --- a/internal/testutil/mocks/session.go +++ b/internal/testutil/mocks/session.go @@ -8,16 +8,16 @@ import ( // SessionData represents session data for testing type SessionData struct { + Claims map[string]interface{} Email string AccessToken string RefreshToken string IDToken string - Expiry int64 Nonce string State string CodeVerifier string RedirectURL string - Claims map[string]interface{} + Expiry int64 } // SessionManager is a testify mock for session management diff --git a/internal/testutil/servers/oidc.go b/internal/testutil/servers/oidc.go index dfd4935..7ace266 100644 --- a/internal/testutil/servers/oidc.go +++ b/internal/testutil/servers/oidc.go @@ -16,45 +16,30 @@ import ( // OIDCServerConfig configures the mock OIDC server behavior type OIDCServerConfig struct { - // Identity - Issuer string - - // Discovery - ScopesSupported []string - ResponseTypesSupported []string + JWKSResponse map[string]interface{} + TokenFixture *fixtures.TokenFixture + UserinfoError *OIDCError + UserinfoResponse map[string]interface{} + IntrospectionResponse map[string]interface{} + JWKSError *OIDCError + RefreshError *OIDCError + TokenResponse map[string]interface{} + TokenError *OIDCError + IntrospectionError *OIDCError + RefreshResponse map[string]interface{} + Issuer string GrantTypesSupported []string - ClaimsSupported []string TokenEndpointAuthMethods []string - - // Token fixture for signing - TokenFixture *fixtures.TokenFixture - - // Token endpoint behavior - TokenResponse map[string]interface{} - TokenError *OIDCError - TokenDelay time.Duration - RefreshResponse map[string]interface{} - RefreshError *OIDCError - - // JWKS behavior - JWKSResponse map[string]interface{} - JWKSError *OIDCError - JWKSDelay time.Duration - - // Introspection behavior - IntrospectionResponse map[string]interface{} - IntrospectionError *OIDCError - - // Userinfo behavior - UserinfoResponse map[string]interface{} - UserinfoError *OIDCError - - // Simulation flags - SimulateTimeout bool - TimeoutDuration time.Duration - RateLimitAfter int - FailAfterN int - FailWithStatus int + ScopesSupported []string + ClaimsSupported []string + ResponseTypesSupported []string + FailAfterN int + JWKSDelay time.Duration + TimeoutDuration time.Duration + RateLimitAfter int + TokenDelay time.Duration + FailWithStatus int + SimulateTimeout bool } // OIDCError represents an OAuth error response @@ -67,9 +52,9 @@ type OIDCError struct { type OIDCServer struct { *httptest.Server Config *OIDCServerConfig - RequestCount int32 - mu sync.Mutex requests []*http.Request + mu sync.Mutex + RequestCount int32 } // NewOIDCServer creates a new mock OIDC server diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index 0115bf4..3944b66 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -135,9 +135,9 @@ func TestIsTestMode(t *testing.T) { // We'll test what we can control via environment variables. tests := []struct { - name string setup func() cleanup func() + name string expected bool }{ { @@ -206,8 +206,8 @@ func TestIsTestMode(t *testing.T) { func TestIsTestModeEdgeCases(t *testing.T) { // Test with various environment variable combinations tests := []struct { - name string env map[string]string + name string }{ { name: "all env vars empty", @@ -560,11 +560,11 @@ func TestIsTestModeYaegiCompiler(t *testing.T) { // mockLogger is a simple mock implementation for testing type mockLogger struct { + lastFormat string + lastArgs []interface{} infoCalls int debugCalls int errorCalls int - lastFormat string - lastArgs []interface{} } func (m *mockLogger) Infof(format string, args ...interface{}) { diff --git a/jwk.go b/jwk.go index a6b6e52..7903b6d 100644 --- a/jwk.go +++ b/jwk.go @@ -21,25 +21,16 @@ import ( // JWK represents a JSON Web Key as defined in RFC 7517. // It can represent different key types including RSA, EC, and symmetric keys. type JWK struct { - // Key type (e.g., "RSA", "EC", "oct") - Kty string `json:"kty"` - // Key use (e.g., "sig" for signature, "enc" for encryption) - Use string `json:"use,omitempty"` - // Key operations allowed + Kty string `json:"kty"` + Use string `json:"use,omitempty"` + Alg string `json:"alg,omitempty"` + Kid string `json:"kid,omitempty"` + N string `json:"n,omitempty"` + E string `json:"e,omitempty"` + Crv string `json:"crv,omitempty"` + X string `json:"x,omitempty"` + Y string `json:"y,omitempty"` KeyOps []string `json:"key_ops,omitempty"` - // Algorithm intended for use with this key - Alg string `json:"alg,omitempty"` - // Key ID - Kid string `json:"kid,omitempty"` - - // RSA specific fields - N string `json:"n,omitempty"` // Modulus - E string `json:"e,omitempty"` // Exponent - - // EC specific fields - Crv string `json:"crv,omitempty"` // Curve - X string `json:"x,omitempty"` // X coordinate - Y string `json:"y,omitempty"` // Y coordinate } // JWKSet represents a set of JSON Web Keys. diff --git a/jwk_caching_test.go b/jwk_caching_test.go index 1db0293..901a0f7 100644 --- a/jwk_caching_test.go +++ b/jwk_caching_test.go @@ -309,18 +309,18 @@ func TestJWKCacheCleanupAndClose(t *testing.T) { func TestFetchJWKSEdgeCases(t *testing.T) { t.Run("handles various HTTP status codes", func(t *testing.T) { testCases := []struct { + errContains string status int wantErr bool - errContains string }{ - {200, false, ""}, - {400, true, "400"}, - {401, true, "401"}, - {403, true, "403"}, - {404, true, "404"}, - {500, true, "500"}, - {502, true, "502"}, - {503, true, "503"}, + {status: 200, wantErr: false, errContains: ""}, + {status: 400, wantErr: true, errContains: "400"}, + {status: 401, wantErr: true, errContains: "401"}, + {status: 403, wantErr: true, errContains: "403"}, + {status: 404, wantErr: true, errContains: "404"}, + {status: 500, wantErr: true, errContains: "500"}, + {status: 502, wantErr: true, errContains: "502"}, + {status: 503, wantErr: true, errContains: "503"}, } for _, tc := range testCases { diff --git a/main_exchange_test.go b/main_exchange_test.go index 84da870..a0552a3 100644 --- a/main_exchange_test.go +++ b/main_exchange_test.go @@ -18,15 +18,15 @@ import ( // TestExchangeCodeForToken_Comprehensive tests the ExchangeCodeForToken function comprehensively func TestExchangeCodeForToken_Comprehensive(t *testing.T) { tests := []struct { + setupMock func(*httptest.Server) *TraefikOidc + validateFunc func(*testing.T, *TokenResponse, error) name string grantType string code string redirectURL string codeVerifier string - setupMock func(*httptest.Server) *TraefikOidc - validateFunc func(*testing.T, *TokenResponse, error) - wantErr bool expectedError string + wantErr bool }{ { name: "successful authorization code exchange", diff --git a/main_goroutine_leak_test.go b/main_goroutine_leak_test.go index 927813d..bf06c98 100644 --- a/main_goroutine_leak_test.go +++ b/main_goroutine_leak_test.go @@ -13,9 +13,9 @@ import ( func TestGoroutineLeakPrevention_ContextCancellation(t *testing.T) { tests := []struct { name string - cancelAfter time.Duration - expectedLeaks int // Maximum expected goroutines after cleanup description string + cancelAfter time.Duration + expectedLeaks int }{ { name: "immediate_cancellation", diff --git a/main_initialization_test.go b/main_initialization_test.go index b0dc7d4..45b9438 100644 --- a/main_initialization_test.go +++ b/main_initialization_test.go @@ -15,10 +15,10 @@ import ( // TestInitializeMetadata tests the initializeMetadata function func TestInitializeMetadata(t *testing.T) { tests := []struct { - name string - providerURL string setupMock func() *httptest.Server validateFunc func(*testing.T, *TraefikOidc) + name string + providerURL string wantPanic bool }{ { diff --git a/main_refresh_test.go b/main_refresh_test.go index efb6143..1f87e37 100644 --- a/main_refresh_test.go +++ b/main_refresh_test.go @@ -16,12 +16,12 @@ import ( // TestGetNewTokenWithRefreshToken tests the GetNewTokenWithRefreshToken function func TestGetNewTokenWithRefreshToken(t *testing.T) { tests := []struct { - name string - refreshToken string setupMock func(*httptest.Server) *TraefikOidc validateFunc func(*testing.T, *TokenResponse, error) - wantErr bool + name string + refreshToken string expectedError string + wantErr bool }{ { name: "successful token refresh", diff --git a/main_servehttp_test.go b/main_servehttp_test.go index c10c349..e6db0ef 100644 --- a/main_servehttp_test.go +++ b/main_servehttp_test.go @@ -10,9 +10,9 @@ import ( // TestServeHTTP_ExcludedURLs tests the excluded URLs functionality func TestServeHTTP_ExcludedURLs(t *testing.T) { tests := []struct { + excludedURLs map[string]struct{} name string path string - excludedURLs map[string]struct{} shouldBypass bool }{ { @@ -506,12 +506,12 @@ type MockSessionData struct { idToken string accessToken string refreshToken string - authenticated bool - isDirty bool - redirectCount int csrf string nonce string codeVerifier string + redirectCount int + authenticated bool + isDirty bool } func (m *MockSessionData) GetEmail() string { return m.email } diff --git a/main_simple_test.go b/main_simple_test.go index 27041e8..3c557d2 100644 --- a/main_simple_test.go +++ b/main_simple_test.go @@ -81,11 +81,11 @@ func TestIsTestMode_DefaultBehavior(t *testing.T) { // TestVerifyAudience tests the verifyAudience function func TestVerifyAudience(t *testing.T) { tests := []struct { - name string tokenAudience interface{} + name string expectedAudience string - expectError bool description string + expectError bool }{ { name: "Audience matches", diff --git a/main_test.go b/main_test.go index 13234b5..d9286c4 100644 --- a/main_test.go +++ b/main_test.go @@ -192,9 +192,9 @@ func (ts *TestSuite) Setup() { // MockJWKCache implements JWKCacheInterface type MockJWKCache struct { - mu sync.RWMutex - JWKS *JWKSet Err error + JWKS *JWKSet + mu sync.RWMutex } // Close is a no-op for the mock. diff --git a/memory_leak_test.go b/memory_leak_test.go index 42b5cec..4dc16e8 100644 --- a/memory_leak_test.go +++ b/memory_leak_test.go @@ -42,27 +42,27 @@ func NewMemoryLeakFixesTestSuite() *MemoryLeakFixesTestSuite { // MemoryTestCase defines a memory leak test scenario type MemoryTestCase struct { - name string - component string // "cache", "session", "token", "plugin", "pool" - scenario string // "concurrent", "longrunning", "stress", "lifecycle" - iterations int - concurrency int setup func(*MemoryTestFramework) error execute func(*MemoryTestFramework) error validateLeak func(*testing.T, runtime.MemStats, runtime.MemStats) cleanup func(*MemoryTestFramework) error + name string + component string + scenario string + iterations int + concurrency int } // MemoryTestFramework provides common test infrastructure for memory tests type MemoryTestFramework struct { - t *testing.T cache CacheInterface + ctx context.Context + t *testing.T plugin *TraefikOidc logger *Logger + cancel context.CancelFunc servers []*httptest.Server configs []*Config - ctx context.Context - cancel context.CancelFunc } // NewMemoryTestFramework creates a new test framework instance @@ -97,12 +97,12 @@ func (tf *MemoryTestFramework) Cleanup() { // ConsolidatedMemorySnapshot captures memory statistics at a point in time type ConsolidatedMemorySnapshot struct { Timestamp time.Time + Description string Alloc uint64 TotalAlloc uint64 Sys uint64 - NumGC uint32 Goroutines int - Description string + NumGC uint32 } // VerifyNoGoroutineLeaks checks for goroutine leaks @@ -1601,8 +1601,8 @@ func TestMemoryLeakConsolidated(t *testing.T) { func TestGoroutineLeaks(t *testing.T) { testCases := []struct { - name string test func(t *testing.T) + name string }{ { name: "cache_no_leak", diff --git a/memory_monitor.go b/memory_monitor.go index 319c0f4..8b49bda 100644 --- a/memory_monitor.go +++ b/memory_monitor.go @@ -10,30 +10,24 @@ import ( // MemoryStats holds comprehensive memory statistics type MemoryStats struct { - // Go runtime memory stats - HeapAllocBytes uint64 // bytes allocated and still in use - HeapSysBytes uint64 // bytes obtained from system - HeapIdleBytes uint64 // bytes in idle (unused) spans - HeapInuseBytes uint64 // bytes in in-use spans - HeapReleasedBytes uint64 // bytes released to the OS - HeapObjects uint64 // total number of allocated objects - StackInuseBytes uint64 // bytes in stack spans - StackSysBytes uint64 // bytes obtained from system for stack - GCSysBytes uint64 // bytes used for garbage collection system metadata - NumGoroutines int // number of goroutines that currently exist - LastGCTime time.Time // time of last garbage collection - - // Application-specific memory tracking - SessionCount int // current number of sessions - TaskCount int // current number of background tasks - CacheSize int64 // estimated cache memory usage - ConnectionPools int // number of HTTP connection pools - - // Memory pressure indicators - MemoryPressure MemoryPressureLevel // overall memory pressure level - GCFrequency float64 // garbage collections per minute - - Timestamp time.Time + LastGCTime time.Time + Timestamp time.Time + GCSysBytes uint64 + NumGoroutines int + HeapReleasedBytes uint64 + HeapObjects uint64 + StackInuseBytes uint64 + StackSysBytes uint64 + HeapAllocBytes uint64 + HeapInuseBytes uint64 + HeapIdleBytes uint64 + SessionCount int + TaskCount int + CacheSize int64 + ConnectionPools int + MemoryPressure MemoryPressureLevel + GCFrequency float64 + HeapSysBytes uint64 } // MemoryPressureLevel indicates the current memory pressure @@ -66,22 +60,18 @@ func (mpl MemoryPressureLevel) String() string { // MemoryMonitor provides comprehensive memory monitoring and alerting type MemoryMonitor struct { - logger *Logger - mu sync.RWMutex - lastStats *MemoryStats - lastGCCount uint32 - lastGCTime time.Time - startTime time.Time - alertThresholds MemoryAlertThresholds - - // Memory leak detection - baselineHeap uint64 - heapGrowthRate float64 // bytes per second - suspiciousGrowth bool - - // Goroutine tracking + lastGCTime time.Time + startTime time.Time + lastStats *MemoryStats + logger *Logger + alertThresholds MemoryAlertThresholds baselineGoroutines int + baselineHeap uint64 + heapGrowthRate float64 maxGoroutines int64 + mu sync.RWMutex + lastGCCount uint32 + suspiciousGrowth bool goroutineLeakAlert bool } diff --git a/mocks_test.go b/mocks_test.go index 0476d39..2a96b37 100644 --- a/mocks_test.go +++ b/mocks_test.go @@ -19,31 +19,25 @@ import ( // MockOAuthProvider simulates an OAuth/OIDC provider for testing type MockOAuthProvider struct { - TokenEndpoint string - AuthEndpoint string - JWKSEndpoint string - RevokeEndpoint string - EndSessionEndpoint string - - // Configurable behaviors - TokenExchangeFunc func(grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) - RefreshTokenFunc func(refreshToken string) (*TokenResponse, error) - RevokeTokenFunc func(token, tokenType string) error - JWKSResponseFunc func() ([]byte, error) - - // Simulation flags - SimulateTimeout bool - SimulateRateLimit bool - SimulateServerError bool + TokenExchangeFunc func(grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) + LastRequest *http.Request + JWKSResponseFunc func() ([]byte, error) + RevokeTokenFunc func(token, tokenType string) error + RefreshTokenFunc func(refreshToken string) (*TokenResponse, error) + EndSessionEndpoint string + TokenEndpoint string + RevokeEndpoint string + JWKSEndpoint string + AuthEndpoint string + RequestHistory []*http.Request + LastRequestBody []byte TimeoutDuration time.Duration ResponseDelay time.Duration - - // Request tracking - RequestCount int32 - LastRequest *http.Request - LastRequestBody []byte - RequestHistory []*http.Request - mu sync.Mutex + mu sync.Mutex + RequestCount int32 + SimulateServerError bool + SimulateRateLimit bool + SimulateTimeout bool } // NewMockOAuthProvider creates a new mock OAuth provider with default endpoints @@ -236,22 +230,16 @@ func (m *MockOAuthProvider) Reset() { // MockSessionManager implements a mock session manager for testing type MockSessionManager struct { - Sessions map[string]*SessionData - mu sync.RWMutex - - // Configurable behaviors + Sessions map[string]*SessionData GetSessionFunc func(r *http.Request) (*SessionData, error) SaveSessionFunc func(r *http.Request, w http.ResponseWriter, session *SessionData) error DeleteSessionFunc func(r *http.Request, w http.ResponseWriter) error - - // Simulation flags - SimulateError bool - SimulateNotFound bool - - // Tracking - GetCallCount int32 - SaveCallCount int32 - DeleteCallCount int32 + mu sync.RWMutex + GetCallCount int32 + SaveCallCount int32 + DeleteCallCount int32 + SimulateError bool + SimulateNotFound bool } // NewMockSessionManager creates a new mock session manager @@ -370,23 +358,16 @@ func (m *MockSessionManager) Reset() { // MockHTTPClient implements a mock HTTP client for testing type MockHTTPClient struct { - // Response configuration - ResponseFunc func(req *http.Request) (*http.Response, error) - - // Default response settings - DefaultStatusCode int - DefaultBody string + ResponseFunc func(req *http.Request) (*http.Response, error) DefaultHeaders map[string]string - - // Simulation flags - SimulateTimeout bool - SimulateError bool - TimeoutDuration time.Duration - - // Request tracking - Requests []*http.Request - RequestBodies [][]byte - mu sync.Mutex + DefaultBody string + Requests []*http.Request + RequestBodies [][]byte + DefaultStatusCode int + TimeoutDuration time.Duration + mu sync.Mutex + SimulateTimeout bool + SimulateError bool } // NewMockHTTPClient creates a new mock HTTP client diff --git a/refresh_coordinator.go b/refresh_coordinator.go index bb8a238..c6c64e6 100644 --- a/refresh_coordinator.go +++ b/refresh_coordinator.go @@ -15,38 +15,19 @@ import ( // It implements request coalescing, rate limiting, and circuit breaking // specifically for token refresh operations. type RefreshCoordinator struct { - // inFlightRefreshes tracks active refresh operations by refresh token hash - inFlightRefreshes map[string]*refreshOperation - // refreshMutex protects the inFlightRefreshes map - refreshMutex sync.RWMutex - - // sessionRefreshAttempts tracks refresh attempts per session + inFlightRefreshes map[string]*refreshOperation + cleanupTimers map[string]*time.Timer sessionRefreshAttempts map[string]*refreshAttemptTracker - // attemptsMutex protects sessionRefreshAttempts map - attemptsMutex sync.RWMutex - - // Circuit breaker for refresh operations - circuitBreaker *RefreshCircuitBreaker - - // Configuration - config RefreshCoordinatorConfig - - // Metrics - metrics *RefreshMetrics - - // Logger - logger *Logger - - // Cleanup goroutine control - stopChan chan struct{} - wg sync.WaitGroup - - // delayedCleanupQueue stores items to be cleaned up after delay - // Uses a timer-based approach instead of spawning goroutines per cleanup - delayedCleanupQueue chan delayedCleanupItem - // cleanupTimerPool reuses timers to avoid goroutine-per-cleanup - cleanupTimerMu sync.Mutex - cleanupTimers map[string]*time.Timer + delayedCleanupQueue chan delayedCleanupItem + circuitBreaker *RefreshCircuitBreaker + metrics *RefreshMetrics + logger *Logger + stopChan chan struct{} + config RefreshCoordinatorConfig + wg sync.WaitGroup + attemptsMutex sync.RWMutex + refreshMutex sync.RWMutex + cleanupTimerMu sync.Mutex } // RefreshCoordinatorConfig configures the refresh coordinator behavior @@ -89,18 +70,12 @@ func DefaultRefreshCoordinatorConfig() RefreshCoordinatorConfig { // refreshOperation represents an in-flight refresh operation type refreshOperation struct { - // refreshToken being refreshed (for validation) + startTime time.Time + result *refreshResult + done chan struct{} refreshToken string - // result stores the final result - result *refreshResult - // done signals when the operation is complete - done chan struct{} - // startTime tracks when the operation started - startTime time.Time - // waiterCount tracks number of goroutines waiting - waiterCount int32 - // mutex protects the result field - mutex sync.RWMutex + mutex sync.RWMutex + waiterCount int32 } // refreshResult contains the result of a refresh operation @@ -112,18 +87,12 @@ type refreshResult struct { // refreshAttemptTracker tracks refresh attempts for a session type refreshAttemptTracker struct { - // attempts counts refresh attempts in current window - attempts int32 - // lastAttemptTime is the timestamp of the last attempt - lastAttemptTime time.Time - // windowStartTime is when the current tracking window started - windowStartTime time.Time - // inCooldown indicates if this session is in cooldown - inCooldown bool - // cooldownEndTime is when cooldown period ends - cooldownEndTime time.Time - // consecutiveFailures tracks consecutive refresh failures + lastAttemptTime time.Time + windowStartTime time.Time + cooldownEndTime time.Time + attempts int32 consecutiveFailures int32 + inCooldown bool } // RefreshMetrics tracks coordinator performance metrics @@ -140,18 +109,18 @@ type RefreshMetrics struct { // delayedCleanupItem represents an item scheduled for delayed cleanup type delayedCleanupItem struct { - tokenHash string cleanupAt time.Time + tokenHash string } // RefreshCircuitBreaker implements a circuit breaker specifically for refresh operations type RefreshCircuitBreaker struct { - state int32 // 0=closed, 1=open, 2=half-open - failures int32 lastFailureTime time.Time lastSuccessTime time.Time config RefreshCircuitBreakerConfig mutex sync.RWMutex + state int32 + failures int32 } // RefreshCircuitBreakerConfig configures the refresh circuit breaker diff --git a/regression/regression_test.go b/regression/regression_test.go index 7ad36b8..508c451 100644 --- a/regression/regression_test.go +++ b/regression/regression_test.go @@ -132,7 +132,7 @@ func testIssue53ReverseProxyHTTPS(t *testing.T) { session.SetEmail("user@example.com") // Azure may use opaque access tokens session.SetAccessToken("opaque-azure-access-token") - session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ") + session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ") // trufflehog:ignore session.SetRefreshToken("azure-refresh-token") // Save with proper security @@ -178,9 +178,9 @@ func testIssue53SameSiteCookies(t *testing.T) { testCases := []struct { name string proto string - expectedSecure bool - expectedSameSite http.SameSite description string + expectedSameSite http.SameSite + expectedSecure bool }{ { name: "HTTPS via proxy", @@ -240,9 +240,9 @@ func testIssue60MissingClaimFields(t *testing.T) { testCases := []struct { name string + description string headers []traefikoidc.TemplatedHeader shouldValidate bool - description string }{ { name: "Direct claim access", diff --git a/session.go b/session.go index b1b0ffe..434a27f 100644 --- a/session.go +++ b/session.go @@ -8,6 +8,7 @@ import ( "crypto/subtle" "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "io" "net/http" @@ -66,23 +67,160 @@ func generateSecureRandomString(length int) (string, error) { // These are appended to the cookiePrefix to create full cookie names // #nosec G101 -- These are cookie name suffixes, not hardcoded credentials const ( - mainCookieSuffix = "m" - accessTokenSuffix = "a" - refreshTokenSuffix = "r" - idTokenSuffix = "id" - defaultCookiePrefix = "_oidc_raczylo_" + mainCookieSuffix = "m" + accessTokenSuffix = "a" + refreshTokenSuffix = "r" + idTokenSuffix = "id" + combinedCookieSuffix = "s" // Combined session cookie suffix + defaultCookiePrefix = "_oidc_raczylo_" ) const ( - maxBrowserCookieSize = 3500 + // maxCookieSize is the maximum raw data size per chunk before securecookie encoding. + // + // Browser cookie limit: 4096 bytes + // Securecookie overhead: ~2x (gob + AES encryption + MAC + double base64) + // Safety headroom: 1000 bytes for cookie metadata, headers, edge cases + // + // Math: 1400 raw → ~2800 encoded → safely under (4096 - 1000 = 3096) limit + maxCookieSize = 1400 - maxCookieSize = 1200 + // maxCombinedChunks is the maximum number of chunks allowed for combined session + maxCombinedChunks = 10 absoluteSessionTimeout = 24 * time.Hour minEncryptionKeyLength = 32 ) +// combinedSessionPayload is the JSON structure for combined cookie storage. +// Uses short field names to minimize size. +type combinedSessionPayload struct { + X map[string]interface{} `json:"x,omitempty"` + A string `json:"a,omitempty"` + R string `json:"r,omitempty"` + I string `json:"i,omitempty"` + E string `json:"e,omitempty"` + Cs string `json:"cs,omitempty"` + N string `json:"n,omitempty"` + Cv string `json:"cv,omitempty"` + Ip string `json:"ip,omitempty"` + Ca int64 `json:"ca,omitempty"` + Rc int `json:"rc,omitempty"` + Au bool `json:"au,omitempty"` +} + +// knownSessionKeys are the standard keys that are handled explicitly in the combined payload. +// All other mainSession.Values keys are stored in the X (extra) field. +var knownSessionKeys = map[string]bool{ + "access_token": true, + "refresh_token": true, + "id_token": true, + "email": true, + "authenticated": true, + "csrf": true, + "nonce": true, + "code_verifier": true, + "incoming_path": true, + "created_at": true, + "redirect_count": true, +} + +// compressCombinedPayload compresses the combined session payload using gzip. +// It serializes the payload to JSON, compresses it, and returns base64-encoded data. +// Returns the compressed string and any error encountered. +func compressCombinedPayload(payload *combinedSessionPayload) (string, error) { + jsonData, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("failed to marshal combined payload: %w", err) + } + + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + if _, err := gz.Write(jsonData); err != nil { + return "", fmt.Errorf("failed to compress combined payload: %w", err) + } + if err := gz.Close(); err != nil { + return "", fmt.Errorf("failed to close gzip writer: %w", err) + } + + compressed := base64.StdEncoding.EncodeToString(buf.Bytes()) + return compressed, nil +} + +// decompressCombinedPayload decompresses a base64+gzip encoded combined session payload. +// Returns the deserialized payload and any error encountered. +func decompressCombinedPayload(compressed string) (*combinedSessionPayload, error) { + if compressed == "" { + return nil, fmt.Errorf("empty compressed data") + } + + data, err := base64.StdEncoding.DecodeString(compressed) + if err != nil { + return nil, fmt.Errorf("failed to decode base64: %w", err) + } + + gr, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gr.Close() + + // Limit decompressed size to prevent zip bombs + limitedReader := io.LimitReader(gr, 512*1024) // 512KB max + decompressed, err := io.ReadAll(limitedReader) + if err != nil { + return nil, fmt.Errorf("failed to decompress: %w", err) + } + + var payload combinedSessionPayload + if err := json.Unmarshal(decompressed, &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal combined payload: %w", err) + } + + return &payload, nil +} + +// splitCombinedIntoChunks splits compressed data into chunks of maxCookieSize. +// Returns the chunks and the total number of chunks. +func splitCombinedIntoChunks(data string, chunkSize int) []string { + if len(data) <= chunkSize { + return []string{data} + } + + var chunks []string + for i := 0; i < len(data); i += chunkSize { + end := i + chunkSize + if end > len(data) { + end = len(data) + } + chunks = append(chunks, data[i:end]) + } + return chunks +} + +// assembleCombinedChunks reassembles chunks back into the original compressed data. +// It reads chunks from session values in order (chunk_0, chunk_1, etc.). +func assembleCombinedChunks(sessions []*sessions.Session) string { + if len(sessions) == 0 { + return "" + } + + var parts []string + for i := 0; i < len(sessions); i++ { + session := sessions[i] + if session == nil { + break + } + chunk, ok := session.Values["d"].(string) // "d" for data + if !ok || chunk == "" { + break + } + parts = append(parts, chunk) + } + return strings.Join(parts, "") +} + // compressToken compresses a JWT token using gzip compression if beneficial. // It validates the token format, attempts compression, and verifies the compressed // data can be decompressed correctly. Only compresses if it reduces size. @@ -236,22 +374,22 @@ func decompressTokenInternal(compressed string) string { // session object reuse and supports both HTTP and HTTPS schemes. type SessionManager struct { sessionPool sync.Pool + ctx context.Context store sessions.Store logger *Logger chunkManager *ChunkManager - cookieDomain string - cookiePrefix string // Prefix for cookie names (default: "_oidc_raczylo_") - sessionMaxAge time.Duration // Maximum session age (default: 24 hours) - cleanupMutex sync.RWMutex - forceHTTPS bool - cleanupDone bool - ctx context.Context - cancel context.CancelFunc memoryMonitor *TaskMemoryMonitor + cancel context.CancelFunc + cookieDomain string + cookiePrefix string + sessionMaxAge time.Duration activeSessions int64 poolHits int64 poolMisses int64 + cleanupMutex sync.RWMutex shutdownOnce sync.Once + forceHTTPS bool + cleanupDone bool } // NewSessionManager creates a new SessionManager instance with secure defaults. @@ -312,6 +450,8 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain strin accessTokenChunks: make(map[int]*sessions.Session), refreshTokenChunks: make(map[int]*sessions.Session), idTokenChunks: make(map[int]*sessions.Session), + combinedChunks: make(map[int]*sessions.Session), + useCombinedStorage: true, // Use combined storage by default for new sessions refreshMutex: sync.Mutex{}, sessionMutex: sync.RWMutex{}, dirty: false, @@ -349,6 +489,17 @@ func (sm *SessionManager) idTokenCookieName() string { return sm.cookiePrefix + idTokenSuffix } +// combinedCookieName returns the combined session cookie base name with the configured prefix +// Chunk cookies are named: prefix + "s" + "_" + chunkIndex (e.g., "_oidc_raczylo_s_0") +func (sm *SessionManager) combinedCookieName() string { + return sm.cookiePrefix + combinedCookieSuffix +} + +// combinedChunkCookieName returns the name for a specific combined session chunk +func (sm *SessionManager) combinedChunkCookieName(chunkIndex int) string { + return fmt.Sprintf("%s_%d", sm.combinedCookieName(), chunkIndex) +} + // Shutdown gracefully shuts down the SessionManager and all its background tasks func (sm *SessionManager) Shutdown() error { var shutdownErr error @@ -650,7 +801,7 @@ func (sm *SessionManager) GetSessionMetrics() map[string]interface{} { metrics["force_https"] = sm.forceHTTPS metrics["absolute_timeout_hours"] = sm.sessionMaxAge.Hours() metrics["max_cookie_size"] = maxCookieSize - metrics["max_browser_cookie_size"] = maxBrowserCookieSize + metrics["max_encoded_cookie_size"] = maxCookieSize * 2 // ~2x encoding overhead if cookieStore, ok := sm.store.(*sessions.CookieStore); ok && len(cookieStore.Codecs) > 0 { metrics["has_encryption"] = true @@ -824,9 +975,10 @@ func (sm *SessionManager) CleanupOldCookies(w http.ResponseWriter, r *http.Reque } // GetSession retrieves or creates session data from the HTTP request. -// It loads the main session and all token chunk sessions, performing validation -// and timeout checks. The returned session must be explicitly returned to the pool -// by calling returnToPoolSafely() to prevent memory leaks. +// It first tries to load from combined cookies (new format), falling back to legacy +// cookies if combined cookies don't exist. Performs validation and timeout checks. +// The returned session must be explicitly returned to the pool by calling +// returnToPoolSafely() to prevent memory leaks. // MEMORY LEAK FIX: Session is NOT returned to pool here - caller must call ReturnToPool() when done. // Parameters: // - r: The HTTP request containing session cookies. @@ -853,6 +1005,26 @@ func (sm *SessionManager) GetSession(r *http.Request) (*SessionData, error) { return nil, fmt.Errorf("%s: %w", message, err) } + // Try to load from combined cookies first (new format) + if sm.loadFromCombinedCookies(r, sessionData) { + sessionData.useCombinedStorage = true + sm.logger.Debug("Loaded session from combined cookies") + + // Check session timeout + if sessionData.getCreatedAtUnsafe() > 0 { + if time.Since(time.Unix(sessionData.getCreatedAtUnsafe(), 0)) > sm.sessionMaxAge { + _ = sessionData.Clear(r, nil) // Safe to ignore: session is being invalidated + return handleError(fmt.Errorf("session timeout"), "session expired") + } + } + + return sessionData, nil + } + + // Fall back to legacy cookies + sessionData.useCombinedStorage = false + sm.logger.Debug("Loading session from legacy cookies") + var err error sessionData.mainSession, err = sm.store.Get(r, sm.mainCookieName()) if err != nil { @@ -895,9 +1067,94 @@ func (sm *SessionManager) GetSession(r *http.Request) (*SessionData, error) { sm.getTokenChunkSessions(r, sm.refreshTokenCookieName(), sessionData.refreshTokenChunks) sm.getTokenChunkSessions(r, sm.idTokenCookieName(), sessionData.idTokenChunks) + // If legacy session has data, migrate to combined storage on next save + if !sessionData.mainSession.IsNew { + sessionData.useCombinedStorage = true + sessionData.dirty = true // Mark dirty to trigger migration on save + sm.logger.Debug("Legacy session found, will migrate to combined storage on save") + } + return sessionData, nil } +// loadFromCombinedCookies attempts to load session data from combined cookies. +// Returns true if combined cookies were found and successfully loaded. +func (sm *SessionManager) loadFromCombinedCookies(r *http.Request, sessionData *SessionData) bool { + // Check if first combined chunk exists + firstChunk, err := sm.store.Get(r, sm.combinedChunkCookieName(0)) + if err != nil || firstChunk.IsNew { + return false + } + + // Get total chunk count from first chunk + totalChunks, ok := firstChunk.Values["n"].(int) + if !ok || totalChunks < 1 || totalChunks > maxCombinedChunks { + sm.logger.Debugf("Invalid combined cookie chunk count: %v", firstChunk.Values["n"]) + return false + } + + // Load all chunks + chunkSessions := make([]*sessions.Session, totalChunks) + chunkSessions[0] = firstChunk + sessionData.combinedChunks[0] = firstChunk + + for i := 1; i < totalChunks; i++ { + chunk, err := sm.store.Get(r, sm.combinedChunkCookieName(i)) + if err != nil || chunk.IsNew { + sm.logger.Debugf("Missing combined cookie chunk %d", i) + return false + } + chunkSessions[i] = chunk + sessionData.combinedChunks[i] = chunk + } + + // Assemble and decompress + compressed := assembleCombinedChunks(chunkSessions) + if compressed == "" { + sm.logger.Debug("Failed to assemble combined chunks") + return false + } + + payload, err := decompressCombinedPayload(compressed) + if err != nil { + sm.logger.Debugf("Failed to decompress combined payload: %v", err) + return false + } + + // Hydrate the legacy session objects for compatibility with existing getter methods + // We need to initialize them even though we're using combined storage + sessionData.mainSession, _ = sm.store.Get(r, sm.mainCookieName()) + sessionData.accessSession, _ = sm.store.Get(r, sm.accessTokenCookieName()) + sessionData.refreshSession, _ = sm.store.Get(r, sm.refreshTokenCookieName()) + sessionData.idTokenSession, _ = sm.store.Get(r, sm.idTokenCookieName()) + + // Populate legacy session values from combined payload + sessionData.mainSession.Values["email"] = payload.E + sessionData.mainSession.Values["authenticated"] = payload.Au + sessionData.mainSession.Values["csrf"] = payload.Cs + sessionData.mainSession.Values["nonce"] = payload.N + sessionData.mainSession.Values["code_verifier"] = payload.Cv + sessionData.mainSession.Values["incoming_path"] = payload.Ip + sessionData.mainSession.Values["created_at"] = payload.Ca + sessionData.mainSession.Values["redirect_count"] = payload.Rc + + // Restore extra custom session values + for key, val := range payload.X { + sessionData.mainSession.Values[key] = val + } + + sessionData.accessSession.Values["token"] = payload.A + sessionData.accessSession.Values["compressed"] = false + + sessionData.refreshSession.Values["token"] = payload.R + sessionData.refreshSession.Values["compressed"] = false + + sessionData.idTokenSession.Values["token"] = payload.I + sessionData.idTokenSession.Values["compressed"] = false + + return true +} + // getTokenChunkSessions loads all available token chunk sessions for a given token type. // It iterates through numbered chunk sessions until no more are found, // populating the provided chunks map with the loaded sessions. @@ -920,11 +1177,13 @@ func (sm *SessionManager) getTokenChunkSessions(r *http.Request, baseName string // SessionData represents a user's authentication session with comprehensive token management. // It handles main session data and supports large tokens that need to be // split across multiple cookies due to browser size limitations. +// Supports both legacy (separate cookies) and combined (single compressed cookie) storage. type SessionData struct { manager *SessionManager request *http.Request + // Legacy storage (kept for backward compatibility during migration) mainSession *sessions.Session accessSession *sessions.Session @@ -939,6 +1198,12 @@ type SessionData struct { idTokenChunks map[int]*sessions.Session + // Combined storage (new approach - single compressed cookie) + combinedChunks map[int]*sessions.Session + + // useCombinedStorage indicates whether to use the new combined storage format + useCombinedStorage bool + refreshMutex sync.Mutex sessionMutex sync.RWMutex @@ -966,6 +1231,7 @@ func (sd *SessionData) MarkDirty() { // Save persists all session data including main session and token chunks. // It applies security options, saves all session components, and handles // errors gracefully by continuing to save other components even if one fails. +// Uses combined cookie storage for efficiency when useCombinedStorage is true. // Parameters: // - r: The HTTP request context for security option configuration. // - w: The HTTP response writer for setting session cookies. @@ -978,6 +1244,117 @@ func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error { options := sd.manager.getSessionOptions(isSecure) options = sd.manager.EnhanceSessionSecurity(options, r) + // Use combined storage for new sessions + if sd.useCombinedStorage { + return sd.saveCombined(r, w, options) + } + + // Legacy storage path (for backward compatibility) + return sd.saveLegacy(r, w, options) +} + +// saveCombined saves all session data in a single compressed, chunked cookie. +// This reduces cookie count and total size through combined compression. +func (sd *SessionData) saveCombined(r *http.Request, w http.ResponseWriter, options *sessions.Options) error { + // Build the combined payload + payload := &combinedSessionPayload{ + A: sd.getAccessTokenUnsafe(), + R: sd.getRefreshTokenUnsafe(), + I: sd.getIDTokenUnsafe(), + E: sd.getEmailUnsafe(), + Au: sd.getAuthenticatedUnsafe(), + Cs: sd.getCSRFUnsafe(), + N: sd.getNonceUnsafe(), + Cv: sd.getCodeVerifierUnsafe(), + Ip: sd.getIncomingPathUnsafe(), + Ca: sd.getCreatedAtUnsafe(), + Rc: sd.getRedirectCountUnsafe(), + } + + // Collect extra session values not handled by the standard fields + sd.sessionMutex.RLock() + if sd.mainSession != nil && len(sd.mainSession.Values) > 0 { + extra := make(map[string]interface{}) + for key, val := range sd.mainSession.Values { + keyStr, ok := key.(string) + if !ok { + continue + } + // Skip known session keys that are already in the payload + if knownSessionKeys[keyStr] { + continue + } + // Store the extra value (must be JSON-serializable) + extra[keyStr] = val + } + if len(extra) > 0 { + payload.X = extra + } + } + sd.sessionMutex.RUnlock() + + // Compress the payload + compressed, err := compressCombinedPayload(payload) + if err != nil { + sd.manager.logger.Errorf("Failed to compress combined payload: %v", err) + // Fall back to legacy storage on compression failure + return sd.saveLegacy(r, w, options) + } + + sd.manager.logger.Debugf("Combined session: raw payload compressed to %d bytes", len(compressed)) + + // Split into chunks + chunks := splitCombinedIntoChunks(compressed, maxCookieSize) + if len(chunks) > maxCombinedChunks { + sd.manager.logger.Errorf("Combined session requires %d chunks, exceeds max %d", len(chunks), maxCombinedChunks) + return fmt.Errorf("session data too large: requires %d chunks, max is %d", len(chunks), maxCombinedChunks) + } + + sd.manager.logger.Debugf("Combined session split into %d chunks", len(chunks)) + + var firstErr error + + // Save each chunk + for i, chunkData := range chunks { + cookieName := sd.manager.combinedChunkCookieName(i) + session, err := sd.manager.store.Get(r, cookieName) + if err != nil { + sd.manager.logger.Errorf("Failed to get combined chunk session %s: %v", cookieName, err) + if firstErr == nil { + firstErr = err + } + continue + } + + session.Values["d"] = chunkData // "d" for data + session.Values["n"] = len(chunks) // "n" for total number of chunks + session.Values["i"] = i // "i" for index + session.Options = options + + if err := session.Save(r, w); err != nil { + sd.manager.logger.Errorf("Failed to save combined chunk %d: %v", i, err) + if firstErr == nil { + firstErr = err + } + } + sd.combinedChunks[i] = session + } + + // Expire old combined chunks that are no longer needed + sd.expireOldCombinedChunks(r, w, options, len(chunks)) + + // Expire legacy cookies if they exist (migration) + sd.expireLegacyCookies(r, w, options) + + if firstErr == nil { + sd.dirty = false + } + return firstErr +} + +// saveLegacy saves session data using the old separate cookie approach. +// Kept for backward compatibility during migration. +func (sd *SessionData) saveLegacy(r *http.Request, w http.ResponseWriter, options *sessions.Options) error { sd.mainSession.Options = options sd.accessSession.Options = options sd.refreshSession.Options = options @@ -1002,11 +1379,8 @@ func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error { } saveOrLogError(sd.mainSession, "main") - saveOrLogError(sd.accessSession, "access token") - saveOrLogError(sd.refreshSession, "refresh token") - saveOrLogError(sd.idTokenSession, "ID token") for i, sessionChunk := range sd.accessTokenChunks { @@ -1030,6 +1404,84 @@ func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error { return firstErr } +// expireOldCombinedChunks expires combined cookie chunks that are no longer needed. +func (sd *SessionData) expireOldCombinedChunks(r *http.Request, w http.ResponseWriter, options *sessions.Options, currentChunks int) { + // Expire chunks beyond the current count + for i := currentChunks; i < maxCombinedChunks; i++ { + cookieName := sd.manager.combinedChunkCookieName(i) + session, err := sd.manager.store.Get(r, cookieName) + if err != nil || session.IsNew { + // No more old chunks + break + } + // Expire this chunk + expireOptions := *options + expireOptions.MaxAge = -1 + session.Options = &expireOptions + for k := range session.Values { + delete(session.Values, k) + } + if err := session.Save(r, w); err != nil { + sd.manager.logger.Debugf("Failed to expire old combined chunk %d: %v", i, err) + } + } +} + +// expireLegacyCookies expires old legacy format cookies during migration. +func (sd *SessionData) expireLegacyCookies(r *http.Request, w http.ResponseWriter, options *sessions.Options) { + expireOptions := *options + expireOptions.MaxAge = -1 + + // Helper to expire a legacy session cookie without clearing in-memory values + // IMPORTANT: We must NOT clear values from sessions that sd is holding, + // as store.Get() returns the same cached session object + expireLegacyChunk := func(cookieName string) { + session, err := sd.manager.store.Get(r, cookieName) + if err != nil || session.IsNew { + return // Cookie doesn't exist + } + session.Options = &expireOptions + // Clear values from chunk cookies (not the main session objects) + for k := range session.Values { + delete(session.Values, k) + } + _ = session.Save(r, w) // Best effort + } + + // For main session cookies, only set expiration WITHOUT clearing values + // because sd.mainSession, sd.accessSession, etc. point to these same objects + expireLegacyMain := func(cookieName string) { + session, err := sd.manager.store.Get(r, cookieName) + if err != nil || session.IsNew { + return // Cookie doesn't exist + } + // Just expire the cookie, don't clear values (they're still needed in memory) + session.Options = &expireOptions + _ = session.Save(r, w) // Best effort + } + + // Expire main legacy cookies (don't clear in-memory values) + expireLegacyMain(sd.manager.mainCookieName()) + expireLegacyMain(sd.manager.accessTokenCookieName()) + expireLegacyMain(sd.manager.refreshTokenCookieName()) + expireLegacyMain(sd.manager.idTokenCookieName()) + + // Expire legacy chunk cookies (safe to clear values, they're separate from main sessions) + for i := 0; i < 50; i++ { // Max legacy chunks was 50 + accessChunk := fmt.Sprintf("%s_%d", sd.manager.accessTokenCookieName(), i) + refreshChunk := fmt.Sprintf("%s_%d", sd.manager.refreshTokenCookieName(), i) + idChunk := fmt.Sprintf("%s_%d", sd.manager.idTokenCookieName(), i) + + session, err := sd.manager.store.Get(r, accessChunk) + if err != nil || session.IsNew { + break // No more chunks + } + expireLegacyChunk(accessChunk) + expireLegacyChunk(refreshChunk) + expireLegacyChunk(idChunk) + } +} + // clearSessionValues removes all values from a session and optionally expires it. // This is used during session cleanup and logout operations. // Parameters: @@ -1263,6 +1715,12 @@ func (sd *SessionData) Reset() { resetSession(sd.refreshSession) resetSession(sd.idTokenSession) + // Clear combined chunks + for k, session := range sd.combinedChunks { + resetSession(session) + delete(sd.combinedChunks, k) + } + // Clear redirect count to prevent leaking between sessions if sd.mainSession != nil && sd.mainSession.Values != nil { delete(sd.mainSession.Values, "redirect_count") @@ -1271,6 +1729,7 @@ func (sd *SessionData) Reset() { sd.dirty = false sd.inUse = false sd.request = nil + sd.useCombinedStorage = true // Reset to use combined storage by default // Reset the refresh mutex to ensure clean state // Note: We don't need to lock it since sessionMutex is already held @@ -1886,8 +2345,9 @@ func splitIntoChunks(s string, chunkSize int) []string { // Returns: // - true if the chunk is safe to store, false if it may exceed browser limits. func validateChunkSize(chunkData string) bool { + // Estimate ~50% overhead for encoding, compare against ~2x maxCookieSize limit estimatedEncodedSize := len(chunkData) + (len(chunkData) * 50 / 100) - return estimatedEncodedSize <= maxBrowserCookieSize + return estimatedEncodedSize <= maxCookieSize*2 } // isCorruptionMarker detects if data contains known corruption indicators. @@ -2088,6 +2548,78 @@ func (sd *SessionData) getIDTokenUnsafe() string { return result.Token } +// getRefreshTokenUnsafe retrieves the refresh token without acquiring locks. +// Used when the session mutex is already held to prevent deadlocks. +func (sd *SessionData) getRefreshTokenUnsafe() string { + token, _ := sd.refreshSession.Values["token"].(string) + compressed, _ := sd.refreshSession.Values["compressed"].(bool) + + if sd.manager == nil || sd.manager.chunkManager == nil { + return token + } + + result := sd.manager.chunkManager.GetToken( + token, + compressed, + sd.refreshTokenChunks, + RefreshTokenConfig, + ) + + if result.Error != nil { + // Handle opaque tokens + if token != "" && !compressed && len(sd.refreshTokenChunks) == 0 { + if strings.Count(token, ".") != 2 { + return token + } + } + return "" + } + + return result.Token +} + +// getEmailUnsafe retrieves the email without acquiring locks. +func (sd *SessionData) getEmailUnsafe() string { + email, _ := sd.mainSession.Values["email"].(string) + return email +} + +// getCSRFUnsafe retrieves the CSRF token without acquiring locks. +func (sd *SessionData) getCSRFUnsafe() string { + csrf, _ := sd.mainSession.Values["csrf"].(string) + return csrf +} + +// getNonceUnsafe retrieves the nonce without acquiring locks. +func (sd *SessionData) getNonceUnsafe() string { + nonce, _ := sd.mainSession.Values["nonce"].(string) + return nonce +} + +// getCodeVerifierUnsafe retrieves the code verifier without acquiring locks. +func (sd *SessionData) getCodeVerifierUnsafe() string { + codeVerifier, _ := sd.mainSession.Values["code_verifier"].(string) + return codeVerifier +} + +// getIncomingPathUnsafe retrieves the incoming path without acquiring locks. +func (sd *SessionData) getIncomingPathUnsafe() string { + path, _ := sd.mainSession.Values["incoming_path"].(string) + return path +} + +// getCreatedAtUnsafe retrieves the created_at timestamp without acquiring locks. +func (sd *SessionData) getCreatedAtUnsafe() int64 { + createdAt, _ := sd.mainSession.Values["created_at"].(int64) + return createdAt +} + +// getRedirectCountUnsafe retrieves the redirect count without acquiring locks. +func (sd *SessionData) getRedirectCountUnsafe() int { + count, _ := sd.mainSession.Values["redirect_count"].(int) + return count +} + // SetIDToken stores an ID token with automatic compression and chunking. // It validates the JWT format, compresses if beneficial, and splits into chunks // if the token exceeds cookie size limits. Includes comprehensive validation. diff --git a/session/chunking/chunk_manager.go b/session/chunking/chunk_manager.go index c8efe6a..73e14ee 100644 --- a/session/chunking/chunk_manager.go +++ b/session/chunking/chunk_manager.go @@ -100,13 +100,12 @@ type Logger interface { // and error handling to ensure data integrity and prevent security vulnerabilities // throughout the process. type ChunkManager struct { - logger Logger - mutex *sync.RWMutex - // sessionMap provides bounded session storage to prevent memory leaks + lastCleanup time.Time + logger Logger + mutex *sync.RWMutex sessionMap map[string]*SessionEntry maxSessions int sessionTTL time.Duration - lastCleanup time.Time } // NewChunkManager creates a new ChunkManager instance with proper initialization. @@ -361,8 +360,8 @@ func (cm *ChunkManager) StoreSession(key string, session *sessions.Session) { if shouldEvict { // Find oldest sessions to remove type sessionAge struct { - key string lastUsed time.Time + key string } sessions := make([]sessionAge, 0, currentLocal) diff --git a/session/chunking/chunk_manager_test.go b/session/chunking/chunk_manager_test.go index f7d3bc0..6891037 100644 --- a/session/chunking/chunk_manager_test.go +++ b/session/chunking/chunk_manager_test.go @@ -16,7 +16,7 @@ func TestTokenValidatorJWT(t *testing.T) { validator := NewTokenValidator() // Test valid JWT format (using base64url encoded parts that are long enough) - validJWT := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + validJWT := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" // trufflehog:ignore err := validator.ValidateJWTFormat(validJWT, "test") if err != nil { t.Errorf("Expected valid JWT to pass, got error: %v", err) @@ -186,10 +186,10 @@ func TestTokenConfigValidation(t *testing.T) { func TestSessionMapBounds_HardLimitEnforcement(t *testing.T) { tests := []struct { name string + description string maxSessions int sessionCount int expectEviction bool - description string }{ { name: "within_limit", @@ -760,18 +760,18 @@ func TestValidateJWTContent(t *testing.T) { tests := []struct { name string token string - expectError bool description string + expectError bool }{ { name: "Valid JWT with required ID token claims", - token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImNsaWVudElkIiwiZXhwIjoxNjQ2MDY0MDAwLCJpYXQiOjE2NDYwNjA0MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImNsaWVudElkIiwiZXhwIjoxNjQ2MDY0MDAwLCJpYXQiOjE2NDYwNjA0MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore expectError: false, description: "JWT with all required ID token claims should pass", }, { name: "JWT missing required claims", - token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore expectError: true, description: "JWT missing required claims should fail", }, @@ -810,8 +810,8 @@ func TestValidateJWTHeader(t *testing.T) { tests := []struct { name string header string - expectError bool description string + expectError bool }{ { name: "Valid JWT header", @@ -865,9 +865,9 @@ func TestValidateJWTPayload(t *testing.T) { tests := []struct { name string payload string + description string config TokenConfig expectError bool - description string }{ { name: "Valid ID token payload", @@ -927,8 +927,8 @@ func TestValidateJWTSignature(t *testing.T) { tests := []struct { name string signature string - expectError bool description string + expectError bool }{ { name: "Valid signature", @@ -976,9 +976,9 @@ func TestValidateChunkStructure(t *testing.T) { tests := []struct { name string + description string chunks []ChunkData expectError bool - description string }{ { name: "Valid chunk structure", @@ -1055,11 +1055,11 @@ func TestValidateChunkData(t *testing.T) { config := AccessTokenConfig tests := []struct { - name string chunk ChunkData + name string + description string expectedTotal int expectError bool - description string }{ { name: "Valid chunk data", @@ -1218,13 +1218,13 @@ func TestGetToken(t *testing.T) { cm := NewChunkManager(nil) tests := []struct { - name string mainSession *sessions.Session chunks map[int]*sessions.Session - config TokenConfig + name string expectedToken string - expectError bool description string + config TokenConfig + expectError bool }{ { name: "Token from main session", @@ -1363,8 +1363,8 @@ func TestSerializeTokenToChunks(t *testing.T) { tests := []struct { name string token string - expectError bool description string + expectError bool }{ { name: "Valid token serialization", @@ -1436,10 +1436,10 @@ func TestDeserializeTokenFromChunks(t *testing.T) { tests := []struct { name string - chunks []ChunkData expectedToken string - expectError bool description string + chunks []ChunkData + expectError bool }{ { name: "Valid chunks deserialization", @@ -1522,10 +1522,10 @@ func TestEncodeDecodeChunk(t *testing.T) { cs := NewChunkSerializer(NewNoOpLogger()) tests := []struct { - name string chunk ChunkData - expectError bool + name string description string + expectError bool }{ { name: "Valid chunk encoding/decoding", @@ -1619,10 +1619,10 @@ func TestValidateChunkIntegrity(t *testing.T) { cs := NewChunkSerializer(NewNoOpLogger()) tests := []struct { - name string chunk ChunkData - expectError bool + name string description string + expectError bool }{ { name: "Valid chunk integrity", diff --git a/session/chunking/chunk_serializer.go b/session/chunking/chunk_serializer.go index a2448bc..02967d8 100644 --- a/session/chunking/chunk_serializer.go +++ b/session/chunking/chunk_serializer.go @@ -254,10 +254,10 @@ func (cs *ChunkSerializer) calculateChecksum(content string) string { // ChunkData represents a single chunk of token data type ChunkData struct { - Index int // Position of this chunk in the sequence - Total int // Total number of chunks for this token - Content string // The actual chunk content - Checksum string // Simple checksum for integrity verification + Content string + Checksum string + Index int + Total int } // EstimateChunkCount estimates how many chunks a token will need diff --git a/session_chunk_manager.go b/session_chunk_manager.go index 541cae6..be6d4c9 100644 --- a/session_chunk_manager.go +++ b/session_chunk_manager.go @@ -84,21 +84,19 @@ type TokenRetrievalResult struct { // and error handling to ensure data integrity and prevent security vulnerabilities // throughout the process. type ChunkManager struct { - logger *Logger - mutex *sync.RWMutex - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup // WaitGroup to track background goroutine completion - // sessionMap provides bounded session storage to prevent memory leaks + lastCleanup time.Time + ctx context.Context + mutex *sync.RWMutex + cancel context.CancelFunc sessionMap map[string]*SessionEntry + logger *Logger + wg sync.WaitGroup maxSessions int sessionTTL time.Duration - lastCleanup time.Time - cleanupRunning int32 // atomic flag to prevent concurrent cleanups - // Memory usage tracking bytesAllocated int64 peakSessions int64 cleanupCount int64 + cleanupRunning int32 } // SessionEntry represents a session with expiration tracking @@ -393,7 +391,8 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co return TokenRetrievalResult{Token: "", Error: err} } - if len(chunk) > maxBrowserCookieSize { + // Secondary check: ensure chunk won't exceed browser limit after encoding (~2x overhead) + if len(chunk) > maxCookieSize*2 { err := fmt.Errorf("%s token chunk %d exceeds browser limit (%d bytes)", config.Type, i, len(chunk)) return TokenRetrievalResult{Token: "", Error: err} @@ -1199,8 +1198,8 @@ func (cm *ChunkManager) findOldestSessions(k int) []string { // Collect all timestamps with keys type sessionAge struct { - key string lastUsed time.Time + key string } sessions := make([]sessionAge, 0, len(cm.sessionMap)) diff --git a/session_test.go b/session_test.go index 939afef..691fe7e 100644 --- a/session_test.go +++ b/session_test.go @@ -24,31 +24,31 @@ import ( // SessionTestCase represents a comprehensive session test scenario type SessionTestCase struct { - name string - scenario string // "creation", "validation", "expiration", "persistence", "cleanup", "chunking", "security" - sessionType string // "user", "admin", "api", "guest", "csrf" setup func(*SessionTestFramework) execute func(*SessionTestFramework) error validate func(*testing.T, error, *SessionTestFramework) cleanup func(*SessionTestFramework) - concurrent bool + name string + scenario string + sessionType string + skipReason string iterations int timeout time.Duration - skipReason string + concurrent bool } // SessionTestFramework provides shared test infrastructure for session tests type SessionTestFramework struct { t *testing.T mockProvider *httptest.Server + testTokens map[string]string + metrics *SessionTestMetrics + config *SessionTestConfig requests []*http.Request responses []*httptest.ResponseRecorder - testTokens map[string]string sessionIDs []string - mu sync.RWMutex - metrics *SessionTestMetrics cleanupFuncs []func() - config *SessionTestConfig + mu sync.RWMutex } // SessionTestMetrics tracks test performance metrics @@ -65,12 +65,12 @@ type SessionTestMetrics struct { // SessionTestConfig holds test configuration type SessionTestConfig struct { + CookieDomain string + EncryptionKey string MaxChunkSize int MaxSessions int - EnableHTTPS bool - CookieDomain string SessionTimeout time.Duration - EncryptionKey string + EnableHTTPS bool EnableCompression bool } @@ -2849,9 +2849,9 @@ func TestSessionExpiryVsTokenExpiry(t *testing.T) { scenarios := []struct { name string + expectedBehavior string sessionAge time.Duration tokenExpiry time.Duration - expectedBehavior string sessionShouldExpire bool tokenShouldRefresh bool }{ @@ -2975,10 +2975,10 @@ func TestSessionCleanupOnTokenExpiry(t *testing.T) { scenarios := []struct { name string - tokenExpiry time.Duration - shouldCleanup bool shouldPreserve []string shouldRemove []string + tokenExpiry time.Duration + shouldCleanup bool }{ { name: "Recently expired tokens - preserve session", diff --git a/settings.go b/settings.go index 42a043d..34aafec 100644 --- a/settings.go +++ b/settings.go @@ -27,359 +27,128 @@ type TemplatedHeader struct { // It provides all necessary settings to configure OpenID Connect authentication // with various providers like Auth0, Logto, or any standard OIDC provider. type Config struct { - HTTPClient *http.Client `json:"-"` - OIDCEndSessionURL string `json:"oidcEndSessionURL"` - CookieDomain string `json:"cookieDomain"` - CookiePrefix string `json:"cookiePrefix"` // Prefix for session cookie names (default: "_oidc_raczylo_") - SessionMaxAge int `json:"sessionMaxAge"` // Maximum session age in seconds (default: 86400 = 24 hours) - CallbackURL string `json:"callbackURL"` - LogoutURL string `json:"logoutURL"` - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - // Audience specifies the expected JWT audience claim value. - // If not set, defaults to ClientID for backward compatibility. - // For Auth0 API access tokens with custom audiences, set this to your API identifier. - // For Azure AD with Application ID URI, set to "api://your-app-id". - // Security: This value is validated against the JWT aud claim to prevent token confusion attacks. - Audience string `json:"audience,omitempty"` - PostLogoutRedirectURI string `json:"postLogoutRedirectURI"` - LogLevel string `json:"logLevel"` - SessionEncryptionKey string `json:"sessionEncryptionKey"` - ProviderURL string `json:"providerURL"` - RevocationURL string `json:"revocationURL"` - ExcludedURLs []string `json:"excludedURLs"` - AllowedUserDomains []string `json:"allowedUserDomains"` - AllowedUsers []string `json:"allowedUsers"` - Scopes []string `json:"scopes"` - Headers []TemplatedHeader `json:"headers"` - AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"` - RateLimit int `json:"rateLimit"` - RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"` - ForceHTTPS bool `json:"forceHTTPS"` - EnablePKCE bool `json:"enablePKCE"` - OverrideScopes bool `json:"overrideScopes"` - // StrictAudienceValidation enforces strict audience validation for access tokens. - // When enabled, sessions are rejected if access token validation fails (prevents fallback to ID token). - // This addresses Auth0 Scenario 2 security concerns where access tokens without proper - // audience claims could be accepted based on ID token validation. - // Default: false (backward compatible - allows ID token fallback) - // Recommended: true for production environments requiring strict OAuth 2.0 compliance - StrictAudienceValidation bool `json:"strictAudienceValidation,omitempty"` - // AllowOpaqueTokens enables acceptance of non-JWT (opaque) access tokens. - // When enabled, opaque tokens are validated via OAuth 2.0 Token Introspection (RFC 7662). - // This supports Auth0 Scenario 3 and other providers that issue opaque access tokens. - // Default: false (only JWT access tokens accepted) - // Note: Requires introspection endpoint to be available from provider metadata - AllowOpaqueTokens bool `json:"allowOpaqueTokens,omitempty"` - // RequireTokenIntrospection forces token introspection for all opaque access tokens. - // When enabled, opaque tokens are rejected if introspection endpoint is unavailable. - // When disabled, opaque tokens fall back to ID token validation. - // Default: false (allows fallback to ID token) - // Recommended: true when AllowOpaqueTokens is enabled for maximum security - RequireTokenIntrospection bool `json:"requireTokenIntrospection,omitempty"` - // DisableReplayDetection disables JTI-based replay attack detection. - // Enable this when running multiple Traefik replicas to prevent false positives. - // Each replica maintains its own in-memory JTI cache, so the same valid token - // hitting different replicas will trigger replay detection on subsequent requests. - // - // Security Note: When enabled, the plugin still validates token signatures, - // expiration, and other claims. Only the JTI replay check is disabled. - // Consider using a shared cache backend (Redis/Memcached) if replay detection - // is required in multi-replica scenarios. - // - // Default: false (replay detection enabled) - // Recommended: true for multi-replica deployments - DisableReplayDetection bool `json:"disableReplayDetection,omitempty"` - SecurityHeaders *SecurityHeadersConfig `json:"securityHeaders,omitempty"` - - // Redis configures the Redis cache backend for distributed caching. - // When enabled, provides cache sharing across multiple Traefik replicas. - // Default: nil (disabled - uses in-memory caching) - Redis *RedisConfig `json:"redis,omitempty"` - - // RoleClaimName specifies the JWT claim name to extract user roles from. - // This allows compatibility with different OIDC providers that use different claim names. - // - // Examples: - // - Default (backward compatible): "roles" - // - Auth0 namespaced: "https://myapp.com/roles" - // - Keycloak realm roles: "realm_access.roles" - // - Custom claim: "user_roles" - // - // If not specified, defaults to "roles" for backward compatibility. - // Supports both simple names and namespaced URIs per OIDC specification. - // - // Default: "roles" - RoleClaimName string `json:"roleClaimName,omitempty"` - - // GroupClaimName specifies the JWT claim name to extract user groups from. - // This allows compatibility with different OIDC providers that use different claim names. - // - // Examples: - // - Default (backward compatible): "groups" - // - Auth0 namespaced: "https://myapp.com/groups" - // - Azure AD groups: "groups" - // - Custom claim: "user_groups" - // - // If not specified, defaults to "groups" for backward compatibility. - // Supports both simple names and namespaced URIs per OIDC specification. - // - // Default: "groups" - GroupClaimName string `json:"groupClaimName,omitempty"` - - // UserIdentifierClaim specifies the JWT claim to use as the user identifier. - // This allows authentication for users without email addresses (e.g., Azure AD service accounts). - // - // Examples: - // - Default (backward compatible): "email" - // - Azure AD without email: "sub", "oid", "upn", or "preferred_username" - // - Generic OIDC: "sub" (always present per OIDC spec) - // - // When set to a non-email claim: - // - AllowedUsers will match against this claim value instead of email - // - AllowedUserDomains validation is skipped (domains only apply to email) - // - The session will store this identifier as the user's identity - // - // Default: "email" - UserIdentifierClaim string `json:"userIdentifierClaim,omitempty"` - - // DynamicClientRegistration enables OIDC Dynamic Client Registration (RFC 7591) - // When enabled, the middleware will automatically register as a client with - // the OIDC provider if ClientID/ClientSecret are not provided. DynamicClientRegistration *DynamicClientRegistrationConfig `json:"dynamicClientRegistration,omitempty"` - - // AllowPrivateIPAddresses disables the security check that blocks private/internal IP addresses. - // By default, the plugin rejects URLs containing private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x) - // to prevent SSRF attacks and ensure OIDC providers are publicly accessible. - // - // Enable this option ONLY when: - // - Your OIDC provider (e.g., Keycloak) runs on an internal network with private IPs - // - You have no DNS resolution available for internal services - // - Your entire stack runs in a Docker network or Kubernetes cluster with private addressing - // - // Security Warning: Enabling this option reduces SSRF protection. Only use in trusted - // network environments where the OIDC provider is known and controlled. - // - // Default: false (private IPs are blocked for security) - AllowPrivateIPAddresses bool `json:"allowPrivateIPAddresses,omitempty"` - - // MinimalHeaders reduces the number of headers forwarded to downstream services. - // This helps prevent "431 Request Header Fields Too Large" errors when downstream - // services have limited header buffer sizes. - // - // When enabled (true): - // - Only forwards: X-Forwarded-User - // - Skips: X-Auth-Request-Token (full ID token), X-Auth-Request-Redirect - // - Groups/roles headers (X-User-Groups, X-User-Roles) are still forwarded if configured - // - Custom templated headers are still processed - // - // When disabled (false, default): - // - Forwards all headers: X-Forwarded-User, X-Auth-Request-User, X-Auth-Request-Redirect, - // X-Auth-Request-Token (full ID token) - // - // Use this option when: - // - Downstream services return "431 Request Header Fields Too Large" errors - // - You don't need the full ID token forwarded to backend services - // - You want to reduce request overhead - // - // Default: false (all headers forwarded for backward compatibility) - MinimalHeaders bool `json:"minimalHeaders,omitempty"` + Redis *RedisConfig `json:"redis,omitempty"` + HTTPClient *http.Client `json:"-"` + SecurityHeaders *SecurityHeadersConfig `json:"securityHeaders,omitempty"` + PostLogoutRedirectURI string `json:"postLogoutRedirectURI"` + LogLevel string `json:"logLevel"` + LogoutURL string `json:"logoutURL"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + Audience string `json:"audience,omitempty"` + CookiePrefix string `json:"cookiePrefix"` + CallbackURL string `json:"callbackURL"` + SessionEncryptionKey string `json:"sessionEncryptionKey"` + ProviderURL string `json:"providerURL"` + RevocationURL string `json:"revocationURL"` + UserIdentifierClaim string `json:"userIdentifierClaim,omitempty"` + GroupClaimName string `json:"groupClaimName,omitempty"` + RoleClaimName string `json:"roleClaimName,omitempty"` + CookieDomain string `json:"cookieDomain"` + OIDCEndSessionURL string `json:"oidcEndSessionURL"` + Scopes []string `json:"scopes"` + AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"` + ExcludedURLs []string `json:"excludedURLs"` + AllowedUserDomains []string `json:"allowedUserDomains"` + AllowedUsers []string `json:"allowedUsers"` + Headers []TemplatedHeader `json:"headers"` + RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"` + SessionMaxAge int `json:"sessionMaxAge"` + RateLimit int `json:"rateLimit"` + OverrideScopes bool `json:"overrideScopes"` + DisableReplayDetection bool `json:"disableReplayDetection,omitempty"` + RequireTokenIntrospection bool `json:"requireTokenIntrospection,omitempty"` + AllowOpaqueTokens bool `json:"allowOpaqueTokens,omitempty"` + StrictAudienceValidation bool `json:"strictAudienceValidation,omitempty"` + EnablePKCE bool `json:"enablePKCE"` + ForceHTTPS bool `json:"forceHTTPS"` + AllowPrivateIPAddresses bool `json:"allowPrivateIPAddresses,omitempty"` + MinimalHeaders bool `json:"minimalHeaders,omitempty"` } // RedisConfig configures Redis cache backend settings for distributed caching. // All fields support both JSON and YAML configuration for compatibility with Traefik's // dynamic configuration (labels, YAML files, etc.) type RedisConfig struct { - // Enabled indicates if Redis caching should be used (default: false) - Enabled bool `json:"enabled" yaml:"enabled"` - - // Address is the Redis server address (e.g., "localhost:6379", "redis:6379") - Address string `json:"address" yaml:"address"` - - // Password for Redis authentication (optional, leave empty for no auth) - Password string `json:"password,omitempty" yaml:"password,omitempty"` - - // DB is the Redis database number to use (default: 0) - DB int `json:"db" yaml:"db"` - - // KeyPrefix is the prefix for all Redis keys (default: "traefikoidc:") - KeyPrefix string `json:"keyPrefix" yaml:"keyPrefix"` - - // PoolSize is the maximum number of socket connections (default: 10) - PoolSize int `json:"poolSize" yaml:"poolSize"` - - // ConnectTimeout is the timeout for establishing connections in seconds (default: 5) - ConnectTimeout int `json:"connectTimeout" yaml:"connectTimeout"` - - // ReadTimeout is the timeout for read operations in seconds (default: 3) - ReadTimeout int `json:"readTimeout" yaml:"readTimeout"` - - // WriteTimeout is the timeout for write operations in seconds (default: 3) - WriteTimeout int `json:"writeTimeout" yaml:"writeTimeout"` - - // EnableTLS indicates if TLS should be used for Redis connections (default: false) - EnableTLS bool `json:"enableTLS" yaml:"enableTLS"` - - // TLSSkipVerify skips TLS certificate verification (not recommended for production) - TLSSkipVerify bool `json:"tlsSkipVerify" yaml:"tlsSkipVerify"` - - // CacheMode determines the caching strategy: "redis" (Redis only), "hybrid" (Memory+Redis), "memory" (Memory only) - // Default: "redis" when enabled - CacheMode string `json:"cacheMode" yaml:"cacheMode"` - - // HybridL1Size is the maximum number of items in L1 cache for hybrid mode (default: 500) - HybridL1Size int `json:"hybridL1Size" yaml:"hybridL1Size"` - - // HybridL1MemoryMB is the maximum memory in MB for L1 cache in hybrid mode (default: 10) - HybridL1MemoryMB int64 `json:"hybridL1MemoryMB" yaml:"hybridL1MemoryMB"` - - // EnableCircuitBreaker enables circuit breaker for Redis failures (default: true) - EnableCircuitBreaker bool `json:"enableCircuitBreaker" yaml:"enableCircuitBreaker"` - - // CircuitBreakerThreshold is the number of failures before opening circuit (default: 5) - CircuitBreakerThreshold int `json:"circuitBreakerThreshold" yaml:"circuitBreakerThreshold"` - - // CircuitBreakerTimeout is the timeout in seconds before attempting to close circuit (default: 60) - CircuitBreakerTimeout int `json:"circuitBreakerTimeout" yaml:"circuitBreakerTimeout"` - - // EnableHealthCheck enables periodic health checks for Redis (default: true) - EnableHealthCheck bool `json:"enableHealthCheck" yaml:"enableHealthCheck"` - - // HealthCheckInterval is the interval in seconds between health checks (default: 30) - HealthCheckInterval int `json:"healthCheckInterval" yaml:"healthCheckInterval"` + KeyPrefix string `json:"keyPrefix" yaml:"keyPrefix"` + Address string `json:"address" yaml:"address"` + Password string `json:"password,omitempty" yaml:"password,omitempty"` + CacheMode string `json:"cacheMode" yaml:"cacheMode"` + WriteTimeout int `json:"writeTimeout" yaml:"writeTimeout"` + CircuitBreakerThreshold int `json:"circuitBreakerThreshold" yaml:"circuitBreakerThreshold"` + ConnectTimeout int `json:"connectTimeout" yaml:"connectTimeout"` + ReadTimeout int `json:"readTimeout" yaml:"readTimeout"` + PoolSize int `json:"poolSize" yaml:"poolSize"` + HealthCheckInterval int `json:"healthCheckInterval" yaml:"healthCheckInterval"` + CircuitBreakerTimeout int `json:"circuitBreakerTimeout" yaml:"circuitBreakerTimeout"` + DB int `json:"db" yaml:"db"` + HybridL1Size int `json:"hybridL1Size" yaml:"hybridL1Size"` + HybridL1MemoryMB int64 `json:"hybridL1MemoryMB" yaml:"hybridL1MemoryMB"` + Enabled bool `json:"enabled" yaml:"enabled"` + EnableCircuitBreaker bool `json:"enableCircuitBreaker" yaml:"enableCircuitBreaker"` + TLSSkipVerify bool `json:"tlsSkipVerify" yaml:"tlsSkipVerify"` + EnableHealthCheck bool `json:"enableHealthCheck" yaml:"enableHealthCheck"` + EnableTLS bool `json:"enableTLS" yaml:"enableTLS"` } // DynamicClientRegistrationConfig configures OIDC Dynamic Client Registration (RFC 7591) type DynamicClientRegistrationConfig struct { - // Enabled enables automatic client registration with the OIDC provider - Enabled bool `json:"enabled"` - - // InitialAccessToken is an optional bearer token for protected registration endpoints - // Some providers require this token to authorize new client registrations - InitialAccessToken string `json:"initialAccessToken,omitempty"` - - // RegistrationEndpoint overrides the endpoint discovered from provider metadata - // If empty, uses the registration_endpoint from .well-known/openid-configuration - RegistrationEndpoint string `json:"registrationEndpoint,omitempty"` - - // ClientMetadata contains the client metadata to register - ClientMetadata *ClientRegistrationMetadata `json:"clientMetadata,omitempty"` - - // PersistCredentials determines whether to save registered credentials to a file - // This allows reusing the same client_id/client_secret across restarts - PersistCredentials bool `json:"persistCredentials"` - - // CredentialsFile is the path to store/load registered client credentials - // Defaults to "/tmp/oidc-client-credentials.json" if not specified - CredentialsFile string `json:"credentialsFile,omitempty"` + ClientMetadata *ClientRegistrationMetadata `json:"clientMetadata,omitempty"` + InitialAccessToken string `json:"initialAccessToken,omitempty"` + RegistrationEndpoint string `json:"registrationEndpoint,omitempty"` + CredentialsFile string `json:"credentialsFile,omitempty"` + Enabled bool `json:"enabled"` + PersistCredentials bool `json:"persistCredentials"` } // ClientRegistrationMetadata contains client metadata for dynamic registration (RFC 7591) type ClientRegistrationMetadata struct { - // RedirectURIs is REQUIRED - array of redirect URIs for authorization - RedirectURIs []string `json:"redirect_uris"` - - // ResponseTypes specifies OAuth 2.0 response types (default: ["code"]) - ResponseTypes []string `json:"response_types,omitempty"` - - // GrantTypes specifies OAuth 2.0 grant types (default: ["authorization_code"]) - GrantTypes []string `json:"grant_types,omitempty"` - - // ApplicationType is either "web" (default) or "native" - ApplicationType string `json:"application_type,omitempty"` - - // Contacts is an array of email addresses for responsible parties - Contacts []string `json:"contacts,omitempty"` - - // ClientName is a human-readable name for the client - ClientName string `json:"client_name,omitempty"` - - // LogoURI is a URL pointing to a logo for the client - LogoURI string `json:"logo_uri,omitempty"` - - // ClientURI is a URL of the home page of the client - ClientURI string `json:"client_uri,omitempty"` - - // PolicyURI is a URL pointing to the client's privacy policy - PolicyURI string `json:"policy_uri,omitempty"` - - // TOSURI is a URL pointing to the client's terms of service - TOSURI string `json:"tos_uri,omitempty"` - - // JWKSURI is a URL for the client's JSON Web Key Set - JWKSURI string `json:"jwks_uri,omitempty"` - - // SubjectType is "pairwise" or "public" (provider-specific) - SubjectType string `json:"subject_type,omitempty"` - - // TokenEndpointAuthMethod specifies how the client authenticates at token endpoint - // Values: "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none" - TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` - - // DefaultMaxAge is the default maximum authentication age in seconds - DefaultMaxAge int `json:"default_max_age,omitempty"` - - // RequireAuthTime specifies whether auth_time claim is required in ID token - RequireAuthTime bool `json:"require_auth_time,omitempty"` - - // DefaultACRValues specifies default ACR values - DefaultACRValues []string `json:"default_acr_values,omitempty"` - - // Scope is a space-separated list of scopes (alternative to config.Scopes) - Scope string `json:"scope,omitempty"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` + TOSURI string `json:"tos_uri,omitempty"` + Scope string `json:"scope,omitempty"` + ApplicationType string `json:"application_type,omitempty"` + SubjectType string `json:"subject_type,omitempty"` + ClientName string `json:"client_name,omitempty"` + LogoURI string `json:"logo_uri,omitempty"` + ClientURI string `json:"client_uri,omitempty"` + PolicyURI string `json:"policy_uri,omitempty"` + JWKSURI string `json:"jwks_uri,omitempty"` + ResponseTypes []string `json:"response_types,omitempty"` + Contacts []string `json:"contacts,omitempty"` + RedirectURIs []string `json:"redirect_uris"` + DefaultACRValues []string `json:"default_acr_values,omitempty"` + GrantTypes []string `json:"grant_types,omitempty"` + DefaultMaxAge int `json:"default_max_age,omitempty"` + RequireAuthTime bool `json:"require_auth_time,omitempty"` } // SecurityHeadersConfig configures security headers for the plugin type SecurityHeadersConfig struct { - // Enable security headers (default: true) - Enabled bool `json:"enabled"` - - // Security profile: "default", "strict", "development", "api", or "custom" - Profile string `json:"profile"` - - // Content Security Policy - ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty"` - - // HSTS settings - StrictTransportSecurity bool `json:"strictTransportSecurity"` - StrictTransportSecurityMaxAge int `json:"strictTransportSecurityMaxAge"` // seconds - StrictTransportSecuritySubdomains bool `json:"strictTransportSecuritySubdomains"` - StrictTransportSecurityPreload bool `json:"strictTransportSecurityPreload"` - - // Frame options: "DENY", "SAMEORIGIN", or "ALLOW-FROM uri" - FrameOptions string `json:"frameOptions,omitempty"` - - // Content type options (default: "nosniff") - ContentTypeOptions string `json:"contentTypeOptions,omitempty"` - - // XSS protection (default: "1; mode=block") - XSSProtection string `json:"xssProtection,omitempty"` - - // Referrer policy - ReferrerPolicy string `json:"referrerPolicy,omitempty"` - - // Permissions policy - PermissionsPolicy string `json:"permissionsPolicy,omitempty"` - - // Cross-origin settings - CrossOriginEmbedderPolicy string `json:"crossOriginEmbedderPolicy,omitempty"` - CrossOriginOpenerPolicy string `json:"crossOriginOpenerPolicy,omitempty"` - CrossOriginResourcePolicy string `json:"crossOriginResourcePolicy,omitempty"` - - // CORS settings - CORSEnabled bool `json:"corsEnabled"` - CORSAllowedOrigins []string `json:"corsAllowedOrigins,omitempty"` - CORSAllowedMethods []string `json:"corsAllowedMethods,omitempty"` - CORSAllowedHeaders []string `json:"corsAllowedHeaders,omitempty"` - CORSAllowCredentials bool `json:"corsAllowCredentials"` - CORSMaxAge int `json:"corsMaxAge"` // seconds - - // Custom headers (in addition to standard security headers) - CustomHeaders map[string]string `json:"customHeaders,omitempty"` - - // Security features - DisableServerHeader bool `json:"disableServerHeader"` - DisablePoweredByHeader bool `json:"disablePoweredByHeader"` + CustomHeaders map[string]string `json:"customHeaders,omitempty"` + PermissionsPolicy string `json:"permissionsPolicy,omitempty"` + Profile string `json:"profile"` + ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty"` + CrossOriginResourcePolicy string `json:"crossOriginResourcePolicy,omitempty"` + CrossOriginOpenerPolicy string `json:"crossOriginOpenerPolicy,omitempty"` + CrossOriginEmbedderPolicy string `json:"crossOriginEmbedderPolicy,omitempty"` + FrameOptions string `json:"frameOptions,omitempty"` + ContentTypeOptions string `json:"contentTypeOptions,omitempty"` + XSSProtection string `json:"xssProtection,omitempty"` + ReferrerPolicy string `json:"referrerPolicy,omitempty"` + CORSAllowedHeaders []string `json:"corsAllowedHeaders,omitempty"` + CORSAllowedOrigins []string `json:"corsAllowedOrigins,omitempty"` + CORSAllowedMethods []string `json:"corsAllowedMethods,omitempty"` + StrictTransportSecurityMaxAge int `json:"strictTransportSecurityMaxAge"` + CORSMaxAge int `json:"corsMaxAge"` + StrictTransportSecurityPreload bool `json:"strictTransportSecurityPreload"` + StrictTransportSecuritySubdomains bool `json:"strictTransportSecuritySubdomains"` + CORSEnabled bool `json:"corsEnabled"` + Enabled bool `json:"enabled"` + CORSAllowCredentials bool `json:"corsAllowCredentials"` + StrictTransportSecurity bool `json:"strictTransportSecurity"` + DisableServerHeader bool `json:"disableServerHeader"` + DisablePoweredByHeader bool `json:"disablePoweredByHeader"` } const ( diff --git a/sharded_cache.go b/sharded_cache.go index 59c9b09..c2765e4 100644 --- a/sharded_cache.go +++ b/sharded_cache.go @@ -17,8 +17,8 @@ type ShardedCache struct { // cacheShard represents a single shard with its own mutex and data map. type cacheShard struct { - mu sync.RWMutex items map[string]*shardedCacheItem + mu sync.RWMutex } // shardedCacheItem represents an item in the sharded cache with expiration. diff --git a/singleton_resources.go b/singleton_resources.go index ad5143d..5af8021 100644 --- a/singleton_resources.go +++ b/singleton_resources.go @@ -18,33 +18,20 @@ var ( // ResourceManager manages shared resources across all middleware instances // to prevent duplication and goroutine leaks when Traefik recreates middleware type ResourceManager struct { - // HTTP clients shared across instances - httpClients map[string]*http.Client - clientsMu sync.RWMutex - - // Caches shared across instances - caches map[string]interface{} - cachesMu sync.RWMutex - - // Background tasks registry - tasks map[string]*BackgroundTask - tasksMu sync.RWMutex - - // Goroutine pools for controlled concurrency - pools map[string]*GoroutinePool - poolsMu sync.RWMutex - - // Reference counting for cleanup references map[string]*int32 - referencesMu sync.RWMutex - - // Logger - logger *Logger - - // Shutdown coordination - shutdownOnce sync.Once + caches map[string]interface{} + httpClients map[string]*http.Client + tasks map[string]*BackgroundTask shutdownChan chan struct{} + pools map[string]*GoroutinePool + logger *Logger wg sync.WaitGroup + cachesMu sync.RWMutex + referencesMu sync.RWMutex + poolsMu sync.RWMutex + tasksMu sync.RWMutex + clientsMu sync.RWMutex + shutdownOnce sync.Once } // GetResourceManager returns the global singleton ResourceManager instance @@ -338,17 +325,15 @@ func (rm *ResourceManager) Shutdown(ctx context.Context) error { // GoroutinePool provides a pool of workers for controlled concurrency type GoroutinePool struct { - maxWorkers int taskQueue chan func() - workerWG sync.WaitGroup - shutdownOnce sync.Once shutdownChan chan struct{} logger *Logger - started int32 - - // Condition variable for efficient Wait() without busy-polling taskCond *sync.Cond - pendingTasks int64 // atomic counter for pending tasks + workerWG sync.WaitGroup + maxWorkers int + pendingTasks int64 + shutdownOnce sync.Once + started int32 } // NewGoroutinePool creates a new goroutine pool with the specified max workers @@ -517,10 +502,10 @@ func (p *GoroutinePool) Shutdown(ctx context.Context) error { // GenericCache provides a simple cache implementation for testing type GenericCache struct { data map[string]interface{} - mu sync.RWMutex - ttl time.Duration logger *Logger stopChan chan struct{} + ttl time.Duration + mu sync.RWMutex } // NewGenericCache creates a new generic cache diff --git a/test_config.go b/test_config.go index aa423eb..4e71611 100644 --- a/test_config.go +++ b/test_config.go @@ -9,26 +9,19 @@ import ( // TestConfig manages test execution configuration and performance settings type TestConfig struct { - // Test execution modes - ExtendedTests bool // Run extended/stress tests - LongTests bool // Run long-running performance tests - QuickMode bool // Quick smoke tests only - - // Performance settings - MaxConcurrency int // Maximum concurrent operations - MaxIterations int // Maximum test iterations - DefaultTimeout time.Duration // Default test timeout - MemoryThreshold float64 // Memory growth threshold in MB - GoroutineGrowth int // Acceptable goroutine growth - - // Cache settings for tests - CacheSize int // Default cache size for tests - CleanupInterval time.Duration // Cleanup interval for tests - - // Environment-specific overrides - MemoryStressTest bool // Enable memory stress tests - ConcurrencyTest bool // Enable high concurrency tests - LeakDetection bool // Enable memory leak detection + MemoryThreshold float64 + MaxConcurrency int + MaxIterations int + DefaultTimeout time.Duration + GoroutineGrowth int + CacheSize int + CleanupInterval time.Duration + LongTests bool + QuickMode bool + ExtendedTests bool + MemoryStressTest bool + ConcurrencyTest bool + LeakDetection bool } // NewTestConfig creates a test configuration based on flags and environment diff --git a/test_framework_test.go b/test_framework_test.go index ad83488..27f3e21 100644 --- a/test_framework_test.go +++ b/test_framework_test.go @@ -21,11 +21,11 @@ type TestFramework struct { server *httptest.Server oidc *TraefikOidc config *Config - cleanup []func() mocks *TestMocks fixtures *TestFixtures privateKey *rsa.PrivateKey publicKey *rsa.PublicKey + cleanup []func() mu sync.Mutex } @@ -457,12 +457,12 @@ func GetTestFramework() *TestFramework { // TestScenario represents a test scenario type TestScenario struct { - Name string Setup func(*TestFramework) Request func(*TestFramework) *http.Request - ExpectedStatus int - ExpectedBody string Validate func(*TestFramework, *httptest.ResponseRecorder) + Name string + ExpectedBody string + ExpectedStatus int } // RunScenarios executes a set of test scenarios diff --git a/test_infrastructure.go b/test_infrastructure.go index 6bf8874..72f6417 100644 --- a/test_infrastructure.go +++ b/test_infrastructure.go @@ -17,10 +17,10 @@ import ( // GlobalTestCleanup tracks and cleans up test resources type GlobalTestCleanup struct { - mu sync.Mutex servers []*httptest.Server tasks []*BackgroundTask caches []interface{ Close() } + mu sync.Mutex } var globalCleanup = &GlobalTestCleanup{} @@ -187,13 +187,13 @@ func GetTestDuration(normal time.Duration) time.Duration { // UnifiedMockSession provides a comprehensive mock for the Session interface type UnifiedMockSession struct { - mu sync.RWMutex data map[string]interface{} callCounts map[string]int64 errors map[string]error delays map[string]time.Duration - destroyed bool destroyCount int64 + mu sync.RWMutex + destroyed bool } // NewUnifiedMockSession creates a new mock session with default behavior @@ -326,13 +326,13 @@ func (m *UnifiedMockSession) GetDestroyCount() int64 { // UnifiedMockTokenVerifier provides a comprehensive mock for token verification type UnifiedMockTokenVerifier struct { - mu sync.RWMutex validTokens map[string]bool tokenMetadata map[string]map[string]interface{} callCounts map[string]int64 errors map[string]error delays map[string]time.Duration verificationFunc func(string) error + mu sync.RWMutex } // NewUnifiedMockTokenVerifier creates a new mock token verifier @@ -414,19 +414,19 @@ func (m *UnifiedMockTokenVerifier) VerifyToken(token string) error { // UnifiedMockTokenCache provides a comprehensive mock for token caching type UnifiedMockTokenCache struct { - mu sync.RWMutex cache map[string]TestCacheEntry callCounts map[string]int64 errors map[string]error delays map[string]time.Duration hitRate float64 + mu sync.RWMutex } // TestCacheEntry represents a cached token entry for testing type TestCacheEntry struct { - Token string ExpiresAt time.Time Metadata map[string]interface{} + Token string } // NewUnifiedMockTokenCache creates a new mock token cache @@ -539,39 +539,39 @@ func (m *UnifiedMockTokenCache) Clear() { // TableTestCase represents a standardized test case structure type TableTestCase struct { - Name string - Description string Input interface{} Expected interface{} ExpectedError error Setup func(*testing.T) error Teardown func(*testing.T) error - Timeout time.Duration + Name string + Description string SkipReason string Tags []string + Timeout time.Duration Parallel bool } // MemoryLeakTestCase represents a test case specifically for memory leak detection type MemoryLeakTestCase struct { + Operation func() error + Setup func() error + Teardown func() error Name string Description string - Operation func() error Iterations int MaxGoroutineGrowth int MaxMemoryGrowthMB float64 - Setup func() error - Teardown func() error - GCBetweenRuns bool Timeout time.Duration + GCBetweenRuns bool } // TestSuiteRunner provides utilities for running table-driven tests type TestSuiteRunner struct { - parallelTests bool - timeout time.Duration beforeEach func(*testing.T) afterEach func(*testing.T) + timeout time.Duration + parallelTests bool } // NewTestSuiteRunner creates a new test suite runner diff --git a/test_utils_test.go b/test_utils_test.go index 4f57f33..b9be4ed 100644 --- a/test_utils_test.go +++ b/test_utils_test.go @@ -20,9 +20,9 @@ func generateRandomString(length int) string { // Test createCaseInsensitiveStringMap function func TestCreateCaseInsensitiveStringMap(t *testing.T) { tests := []struct { + expected map[string]struct{} name string items []string - expected map[string]struct{} }{ { name: "Mixed case items", diff --git a/token_introspection.go b/token_introspection.go index c8a00ea..3c05102 100644 --- a/token_introspection.go +++ b/token_introspection.go @@ -16,18 +16,18 @@ import ( // IntrospectionResponse represents the response from an OAuth 2.0 token introspection endpoint. // Per RFC 7662, this contains information about the token's validity and properties. type IntrospectionResponse struct { - Active bool `json:"active"` // REQUIRED - whether the token is currently active - Scope string `json:"scope,omitempty"` // Space-separated list of scopes - ClientID string `json:"client_id,omitempty"` // Client identifier for the token - Username string `json:"username,omitempty"` // Human-readable identifier for the resource owner - TokenType string `json:"token_type,omitempty"` // Type of token (e.g., "Bearer") - Exp int64 `json:"exp,omitempty"` // Expiration time (seconds since epoch) - Iat int64 `json:"iat,omitempty"` // Issued at time (seconds since epoch) - Nbf int64 `json:"nbf,omitempty"` // Not before time (seconds since epoch) - Sub string `json:"sub,omitempty"` // Subject of the token - Aud string `json:"aud,omitempty"` // Intended audience - Iss string `json:"iss,omitempty"` // Issuer - Jti string `json:"jti,omitempty"` // JWT ID + Scope string `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + Username string `json:"username,omitempty"` + TokenType string `json:"token_type,omitempty"` + Sub string `json:"sub,omitempty"` + Aud string `json:"aud,omitempty"` + Iss string `json:"iss,omitempty"` + Jti string `json:"jti,omitempty"` + Exp int64 `json:"exp,omitempty"` + Iat int64 `json:"iat,omitempty"` + Nbf int64 `json:"nbf,omitempty"` + Active bool `json:"active"` } // introspectToken performs OAuth 2.0 Token Introspection (RFC 7662) for an opaque token. diff --git a/token_resilience.go b/token_resilience.go index b93f042..38c6ae6 100644 --- a/token_resilience.go +++ b/token_resilience.go @@ -8,37 +8,21 @@ import ( // TokenResilienceConfig centralizes resilience configuration for token operations type TokenResilienceConfig struct { - // Circuit breaker configuration for token operations - CircuitBreakerEnabled bool + MetadataCacheConfig MetadataCacheResilienceConfig + RetryConfig RetryConfig CircuitBreakerConfig CircuitBreakerConfig - - // Retry configuration for token operations - RetryEnabled bool - RetryConfig RetryConfig - - // Metadata cache progressive grace period configuration - MetadataCacheConfig MetadataCacheResilienceConfig + CircuitBreakerEnabled bool + RetryEnabled bool } // MetadataCacheResilienceConfig defines resilience settings for metadata cache type MetadataCacheResilienceConfig struct { - // EnableProgressiveGracePeriod allows extending cache TTL on failures - EnableProgressiveGracePeriod bool - - // InitialGracePeriod is the first extension when service is unavailable (5 minutes) - InitialGracePeriod time.Duration - - // ExtendedGracePeriod is the second extension for continued failures (15 minutes) - ExtendedGracePeriod time.Duration - - // MaxGracePeriod is the maximum extension allowed (30 minutes for normal, 15 for security-critical) - MaxGracePeriod time.Duration - - // SecurityCriticalMaxGracePeriod enforces Allan's security limit for critical metadata + SecurityCriticalFields []string + InitialGracePeriod time.Duration + ExtendedGracePeriod time.Duration + MaxGracePeriod time.Duration SecurityCriticalMaxGracePeriod time.Duration - - // SecurityCriticalFields defines which metadata fields are security-critical - SecurityCriticalFields []string + EnableProgressiveGracePeriod bool } // DefaultTokenResilienceConfig returns the default resilience configuration for token operations @@ -90,11 +74,11 @@ func DefaultMetadataCacheResilienceConfig() MetadataCacheResilienceConfig { // TokenResilienceManager coordinates resilience mechanisms for token operations type TokenResilienceManager struct { - config TokenResilienceConfig errorRecoveryManager *ErrorRecoveryManager circuitBreaker *CircuitBreaker retryExecutor *RetryExecutor logger *Logger + config TokenResilienceConfig } // NewTokenResilienceManager creates a new token resilience manager diff --git a/token_test.go b/token_test.go index 19269cd..d4b4dce 100644 --- a/token_test.go +++ b/token_test.go @@ -26,10 +26,10 @@ import ( // Test tokens used across multiple test files var ( - ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" - ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" + ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" // trufflehog:ignore + ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" // trufflehog:ignore ValidRefreshToken = "refresh_token_abc123" - MinimalValidJWT = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0." + MinimalValidJWT = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0." // trufflehog:ignore InvalidTokenOneDot = "invalid.token" InvalidTokenNoDots = "invalidtoken" InvalidTokenThreeDots = "invalid..token" @@ -43,8 +43,8 @@ type TestTokens struct { func NewTestTokens() *TestTokens { return &TestTokens{ - validJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", - expiredJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjoxMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", + validJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", // trufflehog:ignore + expiredJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjoxMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", // trufflehog:ignore } } @@ -137,12 +137,12 @@ func TestOpaqueTokenDetection(t *testing.T) { tests := []struct { name string token string - isOpaque bool description string + isOpaque bool }{ { name: "JWT token with 3 parts", - token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore isOpaque: false, description: "Standard JWT with header.payload.signature", }, @@ -218,7 +218,7 @@ func TestOpaqueTokenValidation(t *testing.T) { }, { name: "Valid JWT token", - token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore wantError: false, }, } @@ -253,8 +253,8 @@ func TestOpaqueTokenStorage(t *testing.T) { tests := []struct { name string token string - shouldStore bool description string + shouldStore bool }{ { name: "Valid opaque token", @@ -264,7 +264,7 @@ func TestOpaqueTokenStorage(t *testing.T) { }, { name: "Valid JWT token", - token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore shouldStore: true, description: "Standard JWT with three parts", }, @@ -754,11 +754,11 @@ func TestDetectTokenType(t *testing.T) { } testCases := []struct { - name string jwt *JWT + name string token string - expectedID bool description string + expectedID bool }{ { name: "ID token with nonce", @@ -1233,16 +1233,16 @@ func TestExtractTime(t *testing.T) { validator := NewTokenValidator(nil) tests := []struct { - name string claim interface{} + name string expected bool }{ - {"float64", float64(1609459200), true}, - {"int64", int64(1609459200), true}, - {"int", int(1609459200), true}, - {"string", "not a timestamp", false}, - {"nil", nil, false}, - {"map", map[string]interface{}{}, false}, + {name: "float64", claim: float64(1609459200), expected: true}, + {name: "int64", claim: int64(1609459200), expected: true}, + {name: "int", claim: int(1609459200), expected: true}, + {name: "string", claim: "not a timestamp", expected: false}, + {name: "nil", claim: nil, expected: false}, + {name: "map", claim: map[string]interface{}{}, expected: false}, } for _, tt := range tests { @@ -1556,11 +1556,11 @@ func TestTokenCorruption(t *testing.T) { validJWT := testTokens.CreateLargeValidJWT(100) tests := []struct { + corruptionScenario func(*SessionData) name string tokenSize int iterations int expectConsistent bool - corruptionScenario func(*SessionData) }{ { name: "Small token - multiple retrievals", @@ -1803,14 +1803,14 @@ func TestTokenValidation(t *testing.T) { t.Run("TokenExpiryValidation", func(t *testing.T) { now := time.Now() tests := []struct { - name string exp time.Time + name string expectValid bool }{ - {"Future expiry", now.Add(time.Hour), true}, - {"Just expired", now.Add(-time.Second), false}, - {"Long expired", now.Add(-24 * time.Hour), false}, - {"Far future", now.Add(365 * 24 * time.Hour), true}, + {name: "Future expiry", exp: now.Add(time.Hour), expectValid: true}, + {name: "Just expired", exp: now.Add(-time.Second), expectValid: false}, + {name: "Long expired", exp: now.Add(-24 * time.Hour), expectValid: false}, + {name: "Far future", exp: now.Add(365 * 24 * time.Hour), expectValid: true}, } for _, tt := range tests { diff --git a/token_validator.go b/token_validator.go index e2ca3cf..d70725b 100644 --- a/token_validator.go +++ b/token_validator.go @@ -27,12 +27,12 @@ func NewTokenValidator(logger *Logger) *TokenValidator { // TokenValidationResult contains the result of token validation type TokenValidationResult struct { - Valid bool - TokenType string + Error error Claims map[string]interface{} Expiry *time.Time IssuedAt *time.Time - Error error + TokenType string + Valid bool } // ValidateToken performs comprehensive token validation diff --git a/types.go b/types.go index 607616f..46ebe9d 100644 --- a/types.go +++ b/types.go @@ -55,9 +55,9 @@ type ProviderMetadata struct { JWKSURL string `json:"jwks_uri"` RevokeURL string `json:"revocation_endpoint"` EndSessionURL string `json:"end_session_endpoint"` - IntrospectionURL string `json:"introspection_endpoint,omitempty"` // OAuth 2.0 Token Introspection (RFC 7662) - ScopesSupported []string `json:"scopes_supported,omitempty"` // Supported scopes from discovery - RegistrationURL string `json:"registration_endpoint,omitempty"` // OIDC Dynamic Client Registration (RFC 7591) + IntrospectionURL string `json:"introspection_endpoint,omitempty"` + RegistrationURL string `json:"registration_endpoint,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` } // TraefikOidc is the main middleware struct that implements OIDC authentication for Traefik. @@ -65,16 +65,18 @@ type ProviderMetadata struct { // the complete authentication flow. It's designed to work seamlessly with Traefik's // plugin system and provides flexible configuration options. type TraefikOidc struct { + lastMetadataRetryTime time.Time jwkCache JWKCacheInterface jwtVerifier JWTVerifier ctx context.Context tokenVerifier TokenVerifier next http.Handler tokenExchanger TokenExchanger + tokenBlacklist CacheInterface + tokenTypeCache CacheInterface + introspectionCache CacheInterface initComplete chan struct{} limiter *rate.Limiter - tokenBlacklist CacheInterface - tokenTypeCache CacheInterface // Cache for token type detection results headerTemplates map[string]*template.Template sessionManager *SessionManager tokenCleanupStopChan chan struct{} @@ -94,50 +96,46 @@ type TraefikOidc struct { errorRecoveryManager *ErrorRecoveryManager tokenResilienceManager *TokenResilienceManager goroutineWG *sync.WaitGroup - clientSecret string - clientID string - audience string // Expected JWT audience, defaults to clientID - roleClaimName string // JWT claim name for extracting roles, defaults to "roles" - groupClaimName string // JWT claim name for extracting groups, defaults to "groups" - userIdentifierClaim string // JWT claim for user identification, defaults to "email" + dcrConfig *DynamicClientRegistrationConfig + dynamicClientRegistrar *DynamicClientRegistrar + scopeFilter *ScopeFilter + securityHeadersApplier func(http.ResponseWriter, *http.Request) + userIdentifierClaim string + revocationURL string name string redirURLPath string logoutURLPath string - metadataMu sync.RWMutex // Protects metadata endpoint fields tokenURL string authURL string endSessionURL string postLogoutRedirectURI string jwksURL string issuerURL string - revocationURL string - introspectionURL string // OAuth 2.0 Token Introspection endpoint (RFC 7662) + groupClaimName string + introspectionURL string providerURL string + roleClaimName string + audience string + clientID string + clientSecret string + registrationURL string + scopesSupported []string scopes []string refreshGracePeriod time.Duration - introspectionCache CacheInterface // Cache for token introspection results + metadataMu sync.RWMutex shutdownOnce sync.Once + metadataRetryMutex sync.Mutex firstRequestMutex sync.Mutex - forceHTTPS bool - enablePKCE bool - overrideScopes bool - strictAudienceValidation bool // Prevents Scenario 2 fallback to ID token - allowOpaqueTokens bool // Enables opaque token support via introspection - requireTokenIntrospection bool // Forces introspection for opaque tokens - disableReplayDetection bool // Disables JTI-based replay detection for multi-replica deployments - suppressDiagnosticLogs bool + minimalHeaders bool firstRequestReceived bool + requireTokenIntrospection bool metadataRefreshStarted bool - lastMetadataRetryTime time.Time // Track last metadata retry for failed state recovery - metadataRetryMutex sync.Mutex // Protects lastMetadataRetryTime - allowPrivateIPAddresses bool // Allow private IP addresses in URLs (for internal networks) - minimalHeaders bool // Reduce headers to prevent 431 errors - securityHeadersApplier func(http.ResponseWriter, *http.Request) - scopeFilter *ScopeFilter // NEW - for discovery-based scope filtering - scopesSupported []string // NEW - from provider metadata - - // Dynamic Client Registration (RFC 7591) - dynamicClientRegistrar *DynamicClientRegistrar - dcrConfig *DynamicClientRegistrationConfig - registrationURL string // OIDC Dynamic Client Registration endpoint + allowPrivateIPAddresses bool + disableReplayDetection bool + allowOpaqueTokens bool + strictAudienceValidation bool + overrideScopes bool + enablePKCE bool + forceHTTPS bool + suppressDiagnosticLogs bool } diff --git a/universal_cache.go b/universal_cache.go index 51b45cc..2c4b779 100644 --- a/universal_cache.go +++ b/universal_cache.go @@ -25,27 +25,21 @@ const ( // UniversalCacheConfig provides configuration for the universal cache type UniversalCacheConfig struct { + Strategy CacheStrategy + Logger *Logger + JWKConfig *JWKCacheConfig + MetadataConfig *MetadataCacheConfig + TokenConfig *TokenCacheConfig Type CacheType - MaxSize int - MaxMemoryBytes int64 DefaultTTL time.Duration CleanupInterval time.Duration - EnableCompression bool + MaxMemoryBytes int64 + MaxSize int + EnableAutoCleanup bool + EnableMemoryLimit bool EnableMetrics bool - EnableAutoCleanup bool // For backward compatibility - EnableMemoryLimit bool // For backward compatibility - Logger *Logger - Strategy CacheStrategy // For backward compatibility - - // SkipAutoCleanup skips starting the per-cache cleanup goroutine. - // Use this when cleanup is managed externally (e.g., by UniversalCacheManager) - // to reduce goroutine count and consolidate cleanup operations. - SkipAutoCleanup bool - - // Type-specific configurations - TokenConfig *TokenCacheConfig - MetadataConfig *MetadataCacheConfig - JWKConfig *JWKCacheConfig + EnableCompression bool + SkipAutoCleanup bool } // TokenCacheConfig provides token-specific cache configuration @@ -57,11 +51,11 @@ type TokenCacheConfig struct { // MetadataCacheConfig provides metadata-specific cache configuration type MetadataCacheConfig struct { + SecurityCriticalFields []string GracePeriod time.Duration ExtendedGracePeriod time.Duration MaxGracePeriod time.Duration SecurityCriticalMaxGracePeriod time.Duration - SecurityCriticalFields []string } // JWKCacheConfig provides JWK-specific cache configuration @@ -73,48 +67,36 @@ type JWKCacheConfig struct { // CacheItem represents a single cache entry type CacheItem struct { - Key string - Value interface{} - Size int64 ExpiresAt time.Time LastAccessed time.Time - AccessCount int64 + Value interface{} + Metadata map[string]interface{} + element *list.Element + Key string CacheType CacheType - - // Type-specific metadata - Metadata map[string]interface{} - - // LRU list element reference - element *list.Element + Size int64 + AccessCount int64 } // UniversalCache provides a single, unified cache implementation // that replaces all other cache types type UniversalCache struct { - mu sync.RWMutex - items map[string]*CacheItem - lruList *list.List - config UniversalCacheConfig - logger *Logger - - // Backend for distributed caching (NEW) - backend backends.CacheBackend - ownsBackend bool // If true, cache should close backend on Close(); if false, backend is shared - - // Memory management - currentSize int64 - currentMemory int64 - - // Metrics - hits int64 - misses int64 - evictions int64 - - // Lifecycle management + config UniversalCacheConfig ctx context.Context + backend backends.CacheBackend + logger *Logger + lruList *list.List + items map[string]*CacheItem cancel context.CancelFunc cleanupTicker *time.Ticker wg sync.WaitGroup + currentSize int64 + currentMemory int64 + hits int64 + misses int64 + evictions int64 + mu sync.RWMutex + ownsBackend bool } // NewUniversalCache creates a new universal cache instance diff --git a/universal_cache_singleton.go b/universal_cache_singleton.go index 8dcfaf4..a3a0879 100644 --- a/universal_cache_singleton.go +++ b/universal_cache_singleton.go @@ -13,22 +13,20 @@ import ( // It runs a single consolidated cleanup goroutine for all caches, reducing // goroutine count and CPU overhead compared to per-cache cleanup routines. type UniversalCacheManager struct { - tokenCache *UniversalCache - blacklistCache *UniversalCache - metadataCache *UniversalCache + sharedBackend backends.CacheBackend + ctx context.Context + tokenTypeCache *UniversalCache jwkCache *UniversalCache sessionCache *UniversalCache - introspectionCache *UniversalCache // OAuth 2.0 Token Introspection cache (RFC 7662) - tokenTypeCache *UniversalCache // Cache for token type detection results - sharedBackend backends.CacheBackend // Shared backend (Redis) that should be closed by manager, not individual caches - mu sync.RWMutex + introspectionCache *UniversalCache + tokenCache *UniversalCache + metadataCache *UniversalCache logger *Logger - - // Consolidated cleanup management - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - cleanupStarted bool + blacklistCache *UniversalCache + cancel context.CancelFunc + wg sync.WaitGroup + mu sync.RWMutex + cleanupStarted bool } var (