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.
This commit is contained in:
2025-12-12 18:35:06 +00:00
committed by GitHub
parent d0b920c4f0
commit 6efb78b7a8
90 changed files with 1529 additions and 1589 deletions
+4 -4
View File
@@ -84,8 +84,8 @@ func TestAudienceValidation(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
audience string audience string
expectError bool
errorContains string errorContains string
expectError bool
}{ }{
{ {
name: "valid custom audience URL", name: "valid custom audience URL",
@@ -163,8 +163,8 @@ func TestConfigAudienceValidation(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
audience string audience string
wantErr bool
errContains string errContains string
wantErr bool
}{ }{
{ {
name: "Empty audience is valid for backward compatibility", name: "Empty audience is valid for backward compatibility",
@@ -732,11 +732,11 @@ func TestJWTAudienceVerification(t *testing.T) {
tokenCache := tc.addTokenCache(NewTokenCache()) tokenCache := tc.addTokenCache(NewTokenCache())
tests := []struct { tests := []struct {
tokenAudience interface{}
name string name string
configAudience string configAudience string
tokenAudience interface{}
wantErr bool
errContains string errContains string
wantErr bool
skipReplayCheck bool skipReplayCheck bool
}{ }{
{ {
+1 -1
View File
@@ -253,8 +253,8 @@ func (s *AuthFlowBehaviourSuite) TestPrepareSessionForAuthentication_WithPKCE()
// TestIsAjaxRequest tests AJAX request detection // TestIsAjaxRequest tests AJAX request detection
func (s *AuthFlowBehaviourSuite) TestIsAjaxRequest() { func (s *AuthFlowBehaviourSuite) TestIsAjaxRequest() {
testCases := []struct { testCases := []struct {
name string
headers map[string]string headers map[string]string
name string
expectAjax bool expectAjax bool
}{ }{
{ {
+10 -11
View File
@@ -222,17 +222,16 @@ func (bt *BackgroundTask) run() {
// TaskCircuitBreaker implements circuit breaker pattern for background task creation // TaskCircuitBreaker implements circuit breaker pattern for background task creation
// It limits concurrent task execution and tracks failures to prevent system overload // It limits concurrent task execution and tracks failures to prevent system overload
type TaskCircuitBreaker struct { type TaskCircuitBreaker struct {
state int32 // CircuitBreakerState
failureCount int32
lastFailureTime int64 // Unix timestamp
failureThreshold int32
timeout time.Duration
logger *Logger logger *Logger
// Concurrency limiting activeTasks map[string]struct{}
concurrentTasks int32 // Current number of running tasks lastFailureTime int64
maxConcurrent int32 // Maximum concurrent tasks allowed timeout time.Duration
activeTasks map[string]struct{} // Track active task names tasksMu sync.RWMutex
tasksMu sync.RWMutex // Separate mutex for task tracking state int32
failureCount int32
failureThreshold int32
concurrentTasks int32
maxConcurrent int32
} }
// NewTaskCircuitBreaker creates a new circuit breaker for background tasks // 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 // TaskRegistry maintains a registry of all active background tasks to prevent duplicates
type TaskRegistry struct { type TaskRegistry struct {
tasks map[string]*BackgroundTask tasks map[string]*BackgroundTask
mu sync.RWMutex
cb *TaskCircuitBreaker cb *TaskCircuitBreaker
logger *Logger logger *Logger
mu sync.RWMutex
} }
// GlobalTaskRegistry is the singleton instance for managing all background tasks // GlobalTaskRegistry is the singleton instance for managing all background tasks
+6 -6
View File
@@ -330,12 +330,12 @@ func TestValidateGoogleTokens(t *testing.T) {
ts.tOidc.refreshGracePeriod = 60 * time.Second ts.tOidc.refreshGracePeriod = 60 * time.Second
tests := []struct { tests := []struct {
name string
setupSession func() *SessionData setupSession func() *SessionData
name string
description string
expectedAuth bool expectedAuth bool
expectedRefresh bool expectedRefresh bool
expectedExpired bool expectedExpired bool
description string
}{ }{
{ {
name: "ValidGoogleTokens", name: "ValidGoogleTokens",
@@ -476,13 +476,13 @@ func TestIsUserAuthenticated(t *testing.T) {
ts.tOidc.refreshGracePeriod = 60 * time.Second ts.tOidc.refreshGracePeriod = 60 * time.Second
tests := []struct { tests := []struct {
setupSession func() *SessionData
name string name string
providerType string providerType string
setupSession func() *SessionData description string
expectedAuth bool expectedAuth bool
expectedRefresh bool expectedRefresh bool
expectedExpired bool expectedExpired bool
description string
}{ }{
{ {
name: "AzureProvider", name: "AzureProvider",
@@ -660,12 +660,12 @@ func TestValidateAzureTokensEdgeCases(t *testing.T) {
ts.tOidc.refreshGracePeriod = 60 * time.Second ts.tOidc.refreshGracePeriod = 60 * time.Second
tests := []struct { tests := []struct {
name string
setupSession func() *SessionData setupSession func() *SessionData
name string
description string
expectedAuth bool expectedAuth bool
expectedRefresh bool expectedRefresh bool
expectedExpired bool expectedExpired bool
description string
}{ }{
{ {
name: "UnauthenticatedWithRefreshToken", name: "UnauthenticatedWithRefreshToken",
+7 -7
View File
@@ -97,15 +97,15 @@ func TestMemoryMonitorComprehensive(t *testing.T) {
t.Run("String method returns pressure name", func(t *testing.T) { t.Run("String method returns pressure name", func(t *testing.T) {
pressures := []struct { pressures := []struct {
level MemoryPressureLevel
name string name string
level MemoryPressureLevel
}{ }{
{MemoryPressureNone, "None"}, {level: MemoryPressureNone, name: "None"},
{MemoryPressureLow, "Low"}, {level: MemoryPressureLow, name: "Low"},
{MemoryPressureModerate, "Moderate"}, {level: MemoryPressureModerate, name: "Moderate"},
{MemoryPressureHigh, "High"}, {level: MemoryPressureHigh, name: "High"},
{MemoryPressureCritical, "Critical"}, {level: MemoryPressureCritical, name: "Critical"},
{MemoryPressureLevel(999), "Unknown"}, {level: MemoryPressureLevel(999), name: "Unknown"},
} }
for _, p := range pressures { for _, p := range pressures {
+3 -3
View File
@@ -155,9 +155,9 @@ type CacheStrategy interface {
// CacheEntry for backward compatibility // CacheEntry for backward compatibility
type CacheEntry struct { type CacheEntry struct {
Key string
Value interface{}
ExpiresAt time.Time ExpiresAt time.Time
Value interface{}
Key string
} }
// Cache is an alias for backward compatibility // Cache is an alias for backward compatibility
@@ -175,10 +175,10 @@ func NewOptimizedCacheWithConfig(config OptimizedCacheConfig) *CacheInterfaceWra
// ListNode for backward compatibility // ListNode for backward compatibility
type ListNode struct { type ListNode struct {
Key string
Value interface{} Value interface{}
Next *ListNode Next *ListNode
Prev *ListNode Prev *ListNode
Key string
} }
// NewFixedMetadataCache creates a metadata cache with fixed configuration // NewFixedMetadataCache creates a metadata cache with fixed configuration
+11 -11
View File
@@ -19,16 +19,16 @@ import (
// CacheTestCase represents a comprehensive test case for cache operations // CacheTestCase represents a comprehensive test case for cache operations
type CacheTestCase struct { type CacheTestCase struct {
setup func(*TestFramework)
execute func(*TestFramework) error
validate func(*testing.T, error, *TestFramework)
cleanup func(*TestFramework)
name string name string
cacheType string // "universal", "metadata", "bounded" cacheType string
operation string // "get", "set", "evict", "cleanup" operation string
setup func(*TestFramework) // Pre-test setup skipReason string
execute func(*TestFramework) error // Test execution timeout time.Duration
validate func(*testing.T, error, *TestFramework) // Validation logic parallel bool
cleanup func(*TestFramework) // Post-test cleanup
timeout time.Duration // Test timeout
parallel bool // Can run in parallel
skipReason string // Optional reason to skip
} }
// createTestCacheConfig creates a standard test configuration // createTestCacheConfig creates a standard test configuration
@@ -698,10 +698,10 @@ func TestUnifiedCache_SetMaxSize(t *testing.T) {
func TestNewCacheAdapter(t *testing.T) { func TestNewCacheAdapter(t *testing.T) {
tests := []struct { tests := []struct {
name string
cache interface{} cache interface{}
expectNil bool name string
description string description string
expectNil bool
}{ }{
{ {
name: "UniversalCache", name: "UniversalCache",
+23 -34
View File
@@ -16,35 +16,26 @@ import (
// ClientRegistrationResponse represents the response from a successful client registration (RFC 7591) // ClientRegistrationResponse represents the response from a successful client registration (RFC 7591)
type ClientRegistrationResponse struct { 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"` 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"` 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) // 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) // DynamicClientRegistrar handles OIDC Dynamic Client Registration (RFC 7591)
type DynamicClientRegistrar struct { type DynamicClientRegistrar struct {
httpClient *http.Client httpClient *http.Client
logger *Logger logger *Logger
config *DynamicClientRegistrationConfig config *DynamicClientRegistrationConfig
providerURL string
// Cached registration response
mu sync.RWMutex
registrationResponse *ClientRegistrationResponse registrationResponse *ClientRegistrationResponse
providerURL string
mu sync.RWMutex
} }
// NewDynamicClientRegistrar creates a new dynamic client registrar // NewDynamicClientRegistrar creates a new dynamic client registrar
+5 -5
View File
@@ -223,10 +223,10 @@ func TestRegisterClientWithInitialAccessToken(t *testing.T) {
// TestRegisterClientError tests error handling during registration // TestRegisterClientError tests error handling during registration
func TestRegisterClientError(t *testing.T) { func TestRegisterClientError(t *testing.T) {
tests := []struct { tests := []struct {
name string
serverResponse func(w http.ResponseWriter, r *http.Request) serverResponse func(w http.ResponseWriter, r *http.Request)
expectError bool name string
errorContains string errorContains string
expectError bool
}{ }{
{ {
name: "invalid_redirect_uri error", name: "invalid_redirect_uri error",
@@ -321,8 +321,8 @@ func TestRegisterClientError(t *testing.T) {
// TestRegisterClientDisabled tests that registration fails when not enabled // TestRegisterClientDisabled tests that registration fails when not enabled
func TestRegisterClientDisabled(t *testing.T) { func TestRegisterClientDisabled(t *testing.T) {
tests := []struct { tests := []struct {
name string
dcrConfig *DynamicClientRegistrationConfig dcrConfig *DynamicClientRegistrationConfig
name string
}{ }{
{ {
name: "nil config", name: "nil config",
@@ -521,8 +521,8 @@ func TestCredentialsValidation(t *testing.T) {
registrar := NewDynamicClientRegistrar(&http.Client{}, NewLogger("DEBUG"), dcrConfig, "https://example.com") registrar := NewDynamicClientRegistrar(&http.Client{}, NewLogger("DEBUG"), dcrConfig, "https://example.com")
tests := []struct { tests := []struct {
name string
response *ClientRegistrationResponse response *ClientRegistrationResponse
name string
expected bool expected bool
}{ }{
{ {
@@ -584,9 +584,9 @@ func TestCredentialsValidation(t *testing.T) {
// TestBuildRegistrationRequest tests the request body construction // TestBuildRegistrationRequest tests the request body construction
func TestBuildRegistrationRequest(t *testing.T) { func TestBuildRegistrationRequest(t *testing.T) {
tests := []struct { tests := []struct {
name string
metadata *ClientRegistrationMetadata metadata *ClientRegistrationMetadata
expectedFields map[string]interface{} expectedFields map[string]interface{}
name string
expectError bool expectError bool
}{ }{
{ {
+29 -47
View File
@@ -12,23 +12,19 @@ import (
// EnhancedMockJWKCache is an improved state-based mock with call tracking // EnhancedMockJWKCache is an improved state-based mock with call tracking
type EnhancedMockJWKCache struct { type EnhancedMockJWKCache struct {
mu sync.RWMutex Err error
JWKS *JWKSet
// State (what to return)
JWKS *JWKSet
Err error
// Call tracking
GetJWKSCalls []JWKSCall GetJWKSCalls []JWKSCall
mu sync.RWMutex
getJWKSCallsMu sync.Mutex
CleanupCalls int32 CleanupCalls int32
CloseCalls int32 CloseCalls int32
getJWKSCallsMu sync.Mutex
} }
// JWKSCall records parameters from a GetJWKS call // JWKSCall records parameters from a GetJWKS call
type JWKSCall struct { type JWKSCall struct {
URL string
Timestamp time.Time Timestamp time.Time
URL string
} }
func (m *EnhancedMockJWKCache) GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error) { 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 // EnhancedMockTokenVerifier is an improved state-based mock with call tracking
type EnhancedMockTokenVerifier struct { type EnhancedMockTokenVerifier struct {
mu sync.RWMutex Err error
VerifyFunc func(token string) error
// State (what to return) - can be a fixed error or a function
Err error
VerifyFunc func(token string) error
// Call tracking
VerifyCalls []TokenVerifyCall VerifyCalls []TokenVerifyCall
mu sync.RWMutex
verifyCallsMu sync.Mutex verifyCallsMu sync.Mutex
} }
// TokenVerifyCall records parameters from a VerifyToken call // TokenVerifyCall records parameters from a VerifyToken call
type TokenVerifyCall struct { type TokenVerifyCall struct {
Token string
Timestamp time.Time Timestamp time.Time
Result error Result error
Token string
} }
func (m *EnhancedMockTokenVerifier) VerifyToken(token string) error { 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 // EnhancedMockTokenExchanger is an improved state-based mock with call tracking
type EnhancedMockTokenExchanger struct { type EnhancedMockTokenExchanger struct {
mu sync.RWMutex
// State (what to return)
ExchangeResponse *TokenResponse
ExchangeErr error
RefreshResponse *TokenResponse
RefreshErr error RefreshErr error
RevokeErr error RevokeErr error
ExchangeErr error
// Optional functions for dynamic behavior
ExchangeCodeFunc func(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) ExchangeCodeFunc func(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error)
RefreshResponse *TokenResponse
ExchangeResponse *TokenResponse
RefreshTokenFunc func(refreshToken string) (*TokenResponse, error) RefreshTokenFunc func(refreshToken string) (*TokenResponse, error)
RevokeTokenFunc func(token, tokenType string) error RevokeTokenFunc func(token, tokenType string) error
ExchangeCalls []ExchangeCall
// Call tracking RefreshCalls []RefreshCall
ExchangeCalls []ExchangeCall RevokeCalls []RevokeCall
RefreshCalls []RefreshCall mu sync.RWMutex
RevokeCalls []RevokeCall exchangeCallsMu sync.Mutex
exchangeCallsMu sync.Mutex refreshCallsMu sync.Mutex
refreshCallsMu sync.Mutex revokeCallsMu sync.Mutex
revokeCallsMu sync.Mutex
} }
// ExchangeCall records parameters from an ExchangeCodeForToken call // ExchangeCall records parameters from an ExchangeCodeForToken call
type ExchangeCall struct { type ExchangeCall struct {
Timestamp time.Time
GrantType string GrantType string
CodeOrToken string CodeOrToken string
RedirectURL string RedirectURL string
CodeVerifier string CodeVerifier string
Timestamp time.Time
} }
// RefreshCall records parameters from a GetNewTokenWithRefreshToken call // RefreshCall records parameters from a GetNewTokenWithRefreshToken call
type RefreshCall struct { type RefreshCall struct {
RefreshToken string
Timestamp time.Time Timestamp time.Time
RefreshToken string
} }
// RevokeCall records parameters from a RevokeTokenWithProvider call // RevokeCall records parameters from a RevokeTokenWithProvider call
type RevokeCall struct { type RevokeCall struct {
Timestamp time.Time
Token string Token string
TokenType string TokenType string
Timestamp time.Time
} }
func (m *EnhancedMockTokenExchanger) ExchangeCodeForToken(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) { 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 // EnhancedMockCacheInterface is an improved state-based mock for CacheInterface
type EnhancedMockCacheInterface struct { type EnhancedMockCacheInterface struct {
mu sync.RWMutex data map[string]cacheEntry
// Internal storage
data map[string]cacheEntry
maxSize int
// Call tracking
GetCalls []CacheGetCall GetCalls []CacheGetCall
SetCalls []CacheSetCall SetCalls []CacheSetCall
DeleteCalls []string DeleteCalls []string
maxSize int
mu sync.RWMutex
getCalls sync.Mutex getCalls sync.Mutex
setCalls sync.Mutex setCalls sync.Mutex
deleteCalls sync.Mutex deleteCalls sync.Mutex
@@ -423,17 +405,17 @@ type cacheEntry struct {
// CacheGetCall records parameters from a Get call // CacheGetCall records parameters from a Get call
type CacheGetCall struct { type CacheGetCall struct {
Timestamp time.Time
Key string Key string
Found bool Found bool
Timestamp time.Time
} }
// CacheSetCall records parameters from a Set call // CacheSetCall records parameters from a Set call
type CacheSetCall struct { type CacheSetCall struct {
Key string
Value any
TTL time.Duration
Timestamp time.Time Timestamp time.Time
Value any
Key string
TTL time.Duration
} }
// NewEnhancedMockCache creates a new enhanced cache mock // NewEnhancedMockCache creates a new enhanced cache mock
+15 -36
View File
@@ -642,14 +642,10 @@ func (e *HTTPError) Error() string {
// OIDCError represents OIDC-specific errors with context information. // OIDCError represents OIDC-specific errors with context information.
// It provides structured error reporting for authentication and authorization failures. // It provides structured error reporting for authentication and authorization failures.
type OIDCError struct { type OIDCError struct {
// Code identifies the specific error type Cause error
Code string
// Message provides a human-readable description
Message string
// Context contains additional error context (e.g., provider, session details)
Context map[string]interface{} Context map[string]interface{}
// Cause is the underlying error that caused this error Code string
Cause error Message string
} }
// Error returns the string representation of the OIDC error. // 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. // SessionError represents session-related errors with context.
// Used for session management, validation, and storage errors. // Used for session management, validation, and storage errors.
type SessionError struct { type SessionError struct {
// Operation describes what session operation failed Cause error
Operation string Operation string
// Message provides a human-readable description Message string
Message string
// SessionID identifies the session (if available)
SessionID string SessionID string
// Cause is the underlying error that caused this error
Cause error
} }
// Error returns the string representation of the session 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. // TokenError represents token-related errors with validation context.
// Used for JWT validation, token refresh, and token format errors. // Used for JWT validation, token refresh, and token format errors.
type TokenError struct { type TokenError struct {
// TokenType identifies the type of token (id_token, access_token, refresh_token) Cause error
TokenType string TokenType string
// Reason describes why the token is invalid Reason string
Reason string Message string
// Message provides a human-readable description
Message string
// Cause is the underlying error that caused this error
Cause error
} }
// Error returns the string representation of the token error. // 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 // It provides fallback mechanisms when primary services are unavailable and monitors
// service health to automatically recover when services become available again. // service health to automatically recover when services become available again.
type GracefulDegradation struct { type GracefulDegradation struct {
// BaseRecoveryMechanism provides common functionality
*BaseRecoveryMechanism *BaseRecoveryMechanism
// fallbacks stores service-specific fallback implementations fallbacks map[string]func() (interface{}, error)
fallbacks map[string]func() (interface{}, error) healthChecks map[string]func() bool
// healthChecks stores service health check functions
healthChecks map[string]func() bool
// degradedServices tracks which services are currently degraded
degradedServices map[string]time.Time degradedServices map[string]time.Time
// config contains graceful degradation configuration healthCheckTask *BackgroundTask
config GracefulDegradationConfig stopChan chan struct{}
// mutex protects shared state config GracefulDegradationConfig
mutex sync.RWMutex mutex sync.RWMutex
// healthCheckTask manages background health checking shutdownOnce sync.Once
healthCheckTask *BackgroundTask
// stopChan signals shutdown
stopChan chan struct{}
// shutdownOnce ensures shutdown happens only once
shutdownOnce sync.Once
} }
// GracefulDegradationConfig holds configuration for graceful degradation behavior. // GracefulDegradationConfig holds configuration for graceful degradation behavior.
+9 -9
View File
@@ -20,10 +20,10 @@ import (
func TestCircuitBreakerStateTransitions(t *testing.T) { func TestCircuitBreakerStateTransitions(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
failures int
maxFailures int
expectedStateBefore string expectedStateBefore string
expectedStateAfter string expectedStateAfter string
failures int
maxFailures int
}{ }{
{ {
name: "stays closed below threshold", name: "stays closed below threshold",
@@ -543,8 +543,8 @@ func TestRetryExecutorNetworkErrors(t *testing.T) {
}, nil) }, nil)
tests := []struct { tests := []struct {
name string
err error err error
name string
shouldRetry bool shouldRetry bool
}{ }{
{ {
@@ -1647,8 +1647,8 @@ func TestGracefulDegradationFullScenario(t *testing.T) {
func TestIsTraefikDefaultCertError(t *testing.T) { func TestIsTraefikDefaultCertError(t *testing.T) {
tests := []struct { tests := []struct {
name string
err error err error
name string
expected bool expected bool
}{ }{
{ {
@@ -1680,8 +1680,8 @@ func TestIsTraefikDefaultCertError(t *testing.T) {
func TestIsEOFError(t *testing.T) { func TestIsEOFError(t *testing.T) {
tests := []struct { tests := []struct {
name string
err error err error
name string
expected bool expected bool
}{ }{
{ {
@@ -1723,8 +1723,8 @@ func TestIsEOFError(t *testing.T) {
func TestIsCertificateError(t *testing.T) { func TestIsCertificateError(t *testing.T) {
tests := []struct { tests := []struct {
name string
err error err error
name string
expected bool expected bool
}{ }{
{ {
@@ -1811,8 +1811,8 @@ func TestRetryExecutorStartupErrors(t *testing.T) {
_ = NewRetryExecutor(MetadataFetchRetryConfig(), nil) _ = NewRetryExecutor(MetadataFetchRetryConfig(), nil)
tests := []struct { tests := []struct {
name string
err error err error
name string
shouldRetry bool shouldRetry bool
}{ }{
{ {
@@ -1890,8 +1890,8 @@ func TestRetryExecutorIsRetryableErrorIntegration(t *testing.T) {
re := NewRetryExecutor(DefaultRetryConfig(), nil) re := NewRetryExecutor(DefaultRetryConfig(), nil)
tests := []struct { tests := []struct {
name string
err error err error
name string
shouldRetry bool shouldRetry bool
}{ }{
{ {
@@ -1977,9 +1977,9 @@ func circuitBreakerStateToString(state CircuitBreakerState) string {
} }
type mockNetError struct { type mockNetError struct {
msg string
timeout bool timeout bool
temporary bool temporary bool
msg string
} }
func (e *mockNetError) Error() string { return e.msg } func (e *mockNetError) Error() string { return e.msg }
+6 -6
View File
@@ -10,16 +10,16 @@ import (
type GoroutineManager struct { type GoroutineManager struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
wg sync.WaitGroup
mu sync.RWMutex
goroutines map[string]*managedGoroutine goroutines map[string]*managedGoroutine
logger *Logger logger *Logger
wg sync.WaitGroup
mu sync.RWMutex
} }
type managedGoroutine struct { type managedGoroutine struct {
name string
cancel context.CancelFunc
startTime time.Time startTime time.Time
cancel context.CancelFunc
name string
running bool running bool
} }
@@ -149,10 +149,10 @@ func (m *GoroutineManager) GetStatus() map[string]GoroutineStatus {
// GoroutineStatus represents the status of a managed goroutine // GoroutineStatus represents the status of a managed goroutine
type GoroutineStatus struct { type GoroutineStatus struct {
Name string
Running bool
StartTime time.Time StartTime time.Time
Name string
Runtime time.Duration Runtime time.Duration
Running bool
} }
// ErrShutdownTimeout is returned when shutdown times out // ErrShutdownTimeout is returned when shutdown times out
+12 -19
View File
@@ -12,30 +12,23 @@ import (
// HTTPClientConfig provides configuration for creating HTTP clients // HTTPClientConfig provides configuration for creating HTTP clients
type HTTPClientConfig struct { type HTTPClientConfig struct {
// Timeout for the entire request IdleConnTimeout time.Duration
Timeout time.Duration MaxIdleConns int
// MaxRedirects allowed (0 means follow Go's default of 10) ReadBufferSize int
MaxRedirects int
// UseCookieJar enables cookie jar for the client
UseCookieJar bool
// Connection settings
DialTimeout time.Duration DialTimeout time.Duration
KeepAlive time.Duration KeepAlive time.Duration
TLSHandshakeTimeout time.Duration TLSHandshakeTimeout time.Duration
ResponseHeaderTimeout time.Duration ResponseHeaderTimeout time.Duration
ExpectContinueTimeout time.Duration ExpectContinueTimeout time.Duration
IdleConnTimeout time.Duration MaxRedirects int
// Connection pool settings MaxIdleConnsPerHost int
MaxIdleConns int Timeout time.Duration
MaxIdleConnsPerHost int MaxConnsPerHost int
MaxConnsPerHost int WriteBufferSize int
// Buffer settings UseCookieJar bool
WriteBufferSize int ForceHTTP2 bool
ReadBufferSize int DisableKeepAlives bool
// Feature flags DisableCompression bool
ForceHTTP2 bool
DisableKeepAlives bool
DisableCompression bool
} }
// DefaultHTTPClientConfig returns the default configuration for general use // DefaultHTTPClientConfig returns the default configuration for general use
+1 -1
View File
@@ -110,9 +110,9 @@ func TestHTTPClientFactoryValidateHTTPClientConfig(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
errorMsg string
config HTTPClientConfig config HTTPClientConfig
wantError bool wantError bool
errorMsg string
}{ }{
{ {
name: "valid config", name: "valid config",
+6 -6
View File
@@ -12,19 +12,19 @@ import (
// SharedTransportPool manages a pool of shared HTTP transports to prevent connection exhaustion // SharedTransportPool manages a pool of shared HTTP transports to prevent connection exhaustion
type SharedTransportPool struct { type SharedTransportPool struct {
mu sync.RWMutex
transports map[string]*sharedTransport
maxConns int
ctx context.Context ctx context.Context
transports map[string]*sharedTransport
cancel context.CancelFunc cancel context.CancelFunc
clientCount int32 // SECURITY FIX: Track total HTTP clients maxConns int
maxClients int32 // SECURITY FIX: Limit total clients to 5 mu sync.RWMutex
clientCount int32
maxClients int32
} }
type sharedTransport struct { type sharedTransport struct {
lastUsed time.Time
transport *http.Transport transport *http.Transport
refCount int refCount int
lastUsed time.Time
} }
var ( var (
+9 -9
View File
@@ -14,7 +14,7 @@ func TestInputValidator(t *testing.T) {
} }
t.Run("Valid token validation", func(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) result := validator.ValidateToken(validToken)
if !result.IsValid { if !result.IsValid {
@@ -428,12 +428,12 @@ func TestInputValidatorValidateToken(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
token string token string
expectValid bool
description string description string
expectValid bool
}{ }{
{ {
name: "ValidJWTToken", name: "ValidJWTToken",
token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyLCJpYXQiOjE1MTYyMzkwMjJ9.signature", token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyLCJpYXQiOjE1MTYyMzkwMjJ9.signature", // trufflehog:ignore
expectValid: true, expectValid: true,
description: "Valid JWT token should pass validation", description: "Valid JWT token should pass validation",
}, },
@@ -475,7 +475,7 @@ func TestInputValidatorValidateToken(t *testing.T) {
}, },
{ {
name: "MaliciousJWTWithExtraData", name: "MaliciousJWTWithExtraData",
token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig.malicious_extra", token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig.malicious_extra", // trufflehog:ignore
expectValid: false, expectValid: false,
description: "JWT with extra malicious data should fail validation", description: "JWT with extra malicious data should fail validation",
}, },
@@ -500,8 +500,8 @@ func TestInputValidatorValidateEmail(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
email string email string
expectValid bool
description string description string
expectValid bool
}{ }{
{ {
name: "ValidEmail", name: "ValidEmail",
@@ -578,8 +578,8 @@ func TestInputValidatorValidateURL(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
url string url string
expectValid bool
description string description string
expectValid bool
}{ }{
{ {
name: "ValidHTTPSURL", name: "ValidHTTPSURL",
@@ -669,8 +669,8 @@ func TestInputValidatorValidateClaim(t *testing.T) {
name string name string
claimName string claimName string
claimValue string claimValue string
expectValid bool
description string description string
expectValid bool
}{ }{
{ {
name: "ValidStringClaim", name: "ValidStringClaim",
@@ -750,8 +750,8 @@ func TestInputValidatorValidateHeader(t *testing.T) {
name string name string
headerName string headerName string
headerValue string headerValue string
expectValid bool
description string description string
expectValid bool
}{ }{
{ {
name: "ValidHeader", name: "ValidHeader",
@@ -830,8 +830,8 @@ func TestInputValidatorValidateUsername(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
username string username string
expectValid bool
description string description string
expectValid bool
}{ }{
{ {
name: "ValidUsername", name: "ValidUsername",
+4 -4
View File
@@ -726,20 +726,20 @@ type MockConfig struct {
} }
type MockSession struct { type MockSession struct {
id string
userID string
created time.Time created time.Time
lastUsed time.Time lastUsed time.Time
data map[string]interface{} data map[string]interface{}
id string
userID string
} }
type TestResult struct { type TestResult struct {
UserID int
StartTime time.Time StartTime time.Time
EndTime time.Time EndTime time.Time
Error error
UserID int
Duration time.Duration Duration time.Duration
Success bool Success bool
Error error
} }
// ============================================================================ // ============================================================================
+14 -25
View File
@@ -18,33 +18,22 @@ const (
// Config provides common configuration for cache backends // Config provides common configuration for cache backends
type Config struct { type Config struct {
// Type specifies the backend type L2Config *Config
Type BackendType L1Config *Config
RedisPrefix string
// Memory backend settings Type BackendType
MaxSize int RedisAddr string
MaxMemoryBytes int64 RedisPassword string
CleanupInterval time.Duration PoolSize int
RedisDB int
// Redis backend settings CleanupInterval time.Duration
RedisAddr string MaxMemoryBytes int64
RedisPassword string MaxSize int
RedisDB int HealthCheckInterval time.Duration
RedisPrefix string AsyncWrites bool
PoolSize int
// Hybrid backend settings
L1Config *Config // Memory cache (L1)
L2Config *Config // Redis cache (L2)
AsyncWrites bool // Write to L2 asynchronously
// Resilience settings
EnableCircuitBreaker bool EnableCircuitBreaker bool
EnableHealthCheck bool EnableHealthCheck bool
HealthCheckInterval time.Duration EnableMetrics bool
// Metrics
EnableMetrics bool
} }
// DefaultConfig returns a default configuration for in-memory caching // DefaultConfig returns a default configuration for in-memory caching
+18 -28
View File
@@ -13,40 +13,30 @@ import (
// HybridBackend implements a two-tier cache with L1 (memory) and L2 (Redis) backends // 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 // It provides automatic failover, async writes for non-critical data, and optimized read paths
type HybridBackend struct { type HybridBackend struct {
primary CacheBackend // L1: Memory cache for fast access lastL2Error atomic.Value
secondary CacheBackend // L2: Redis cache for distributed access secondary CacheBackend
primary CacheBackend
// Configuration logger Logger
syncWriteCacheTypes map[string]bool // Which cache types require synchronous writes ctx context.Context
syncWriteCacheTypes map[string]bool
asyncWriteBuffer chan *asyncWriteItem asyncWriteBuffer chan *asyncWriteItem
cancel context.CancelFunc
// Metrics wg sync.WaitGroup
l1Hits atomic.Int64 l1Hits atomic.Int64
l2Hits atomic.Int64 errors atomic.Int64
misses atomic.Int64 l2Writes atomic.Int64
l1Writes atomic.Int64 l1Writes atomic.Int64
l2Writes atomic.Int64 misses atomic.Int64
errors atomic.Int64 l2Hits atomic.Int64
fallbackMode atomic.Bool
// 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
} }
// asyncWriteItem represents an async write operation // asyncWriteItem represents an async write operation
type asyncWriteItem struct { type asyncWriteItem struct {
ctx context.Context
key string key string
value []byte value []byte
ttl time.Duration ttl time.Duration
ctx context.Context
} }
// Logger interface for structured logging // Logger interface for structured logging
@@ -82,9 +72,9 @@ func (l *defaultLogger) Errorf(format string, args ...interface{}) {
type HybridConfig struct { type HybridConfig struct {
Primary CacheBackend Primary CacheBackend
Secondary CacheBackend Secondary CacheBackend
SyncWriteCacheTypes map[string]bool // Cache types requiring synchronous L2 writes
AsyncBufferSize int
Logger Logger Logger Logger
SyncWriteCacheTypes map[string]bool
AsyncBufferSize int
} }
// NewHybridBackend creates a new hybrid cache backend with L1 (memory) and L2 (Redis) tiers // NewHybridBackend creates a new hybrid cache backend with L1 (memory) and L2 (Redis) tiers
+6 -6
View File
@@ -17,23 +17,23 @@ import (
// mockBackend is a simple mock implementation of CacheBackend for testing // mockBackend is a simple mock implementation of CacheBackend for testing
type mockBackend struct { type mockBackend struct {
pingError error
data map[string]mockEntry data map[string]mockEntry
stats map[string]interface{}
mu sync.RWMutex mu sync.RWMutex
getCalls atomic.Int32
setCalls atomic.Int32
deleteCalls atomic.Int32
failSet bool failSet bool
failGet bool failGet bool
failDelete bool failDelete bool
failClear bool failClear bool
failPing bool failPing bool
pingError error
stats map[string]interface{}
getCalls atomic.Int32
setCalls atomic.Int32
deleteCalls atomic.Int32
} }
type mockEntry struct { type mockEntry struct {
value []byte
expiresAt time.Time expiresAt time.Time
value []byte
} }
// mockBatchBackend extends mockBackend with batch operations // mockBatchBackend extends mockBackend with batch operations
+14 -45
View File
@@ -41,53 +41,22 @@ type CacheBackend interface {
// BackendStats represents statistics for a cache backend // BackendStats represents statistics for a cache backend
type BackendStats struct { type BackendStats struct {
// Type is the backend type StartTime time.Time
Type BackendType LastErrorTime time.Time
Type BackendType
// Hits is the number of cache hits LastError string
Hits int64 Deletes int64
Errors int64
// Misses is the number of cache misses Evictions int64
Misses int64 CurrentSize int64
MaxSize int64
// Sets is the number of set operations MemoryUsage int64
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
AverageGetLatency time.Duration AverageGetLatency time.Duration
// AverageSetLatency is the average latency for set operations
AverageSetLatency time.Duration AverageSetLatency time.Duration
Sets int64
// LastError is the last error encountered Misses int64
LastError string Uptime time.Duration
Hits int64
// 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
} }
// BackendCapabilities describes the capabilities of a cache backend // BackendCapabilities describes the capabilities of a cache backend
+27 -35
View File
@@ -11,14 +11,14 @@ import (
// memoryCacheItem represents an item in the memory cache // memoryCacheItem represents an item in the memory cache
type memoryCacheItem struct { type memoryCacheItem struct {
key string
value interface{}
expiresAt time.Time expiresAt time.Time
createdAt time.Time createdAt time.Time
accessedAt time.Time accessedAt time.Time
value interface{}
element *list.Element
key string
accessCount int64 accessCount int64
size int64 size int64
element *list.Element // for LRU tracking
} }
// isExpired checks if the item is expired // 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 // MemoryCacheBackend implements the CacheBackend interface using in-memory storage
type MemoryCacheBackend struct { type MemoryCacheBackend struct {
mu sync.RWMutex startTime time.Time
items map[string]*memoryCacheItem lastErrorTime time.Time
lruList *list.List items map[string]*memoryCacheItem
maxSize int64 lruList *list.List
maxMemory int64 cleanupDone chan bool
currentSize int64 cleanupTicker *time.Ticker
currentMemory int64 evictionPolicy string
lastError string
// Statistics currentMemory int64
hits atomic.Int64 misses atomic.Int64
misses atomic.Int64 deletes atomic.Int64
sets atomic.Int64 evictions atomic.Int64
deletes atomic.Int64 errors atomic.Int64
evictions atomic.Int64 totalGetTime atomic.Int64
errors atomic.Int64 totalSetTime atomic.Int64
getCount atomic.Int64
// Latency tracking setCount atomic.Int64
totalGetTime atomic.Int64 sets atomic.Int64
totalSetTime atomic.Int64 hits atomic.Int64
getCount atomic.Int64 maxSize int64
setCount atomic.Int64 currentSize int64
maxMemory int64
// Status
startTime time.Time
lastError string
lastErrorTime time.Time
cleanupTicker *time.Ticker
cleanupDone chan bool
closed atomic.Bool
// Configuration
cleanupInterval time.Duration cleanupInterval time.Duration
evictionPolicy string // "lru", "lfu", "fifo" mu sync.RWMutex
closed atomic.Bool
} }
// NewMemoryCacheBackend creates a new memory cache backend // NewMemoryCacheBackend creates a new memory cache backend
+10 -16
View File
@@ -9,30 +9,24 @@ import (
// HealthMonitor continuously monitors Redis connection health and triggers reconnections // HealthMonitor continuously monitors Redis connection health and triggers reconnections
type HealthMonitor struct { type HealthMonitor struct {
pool *ConnectionPool pool *ConnectionPool
config *HealthMonitorConfig config *HealthMonitorConfig
stopChan chan struct{}
// State wg sync.WaitGroup
healthy atomic.Bool lastCheckTime atomic.Int64
running atomic.Bool
lastCheckTime atomic.Int64 // Unix timestamp
// Metrics
consecutiveFailures atomic.Int64 consecutiveFailures atomic.Int64
totalChecks atomic.Int64 totalChecks atomic.Int64
totalFailures atomic.Int64 totalFailures atomic.Int64
healthy atomic.Bool
// Lifecycle running atomic.Bool
stopChan chan struct{}
wg sync.WaitGroup
} }
// HealthMonitorConfig configures the health monitor // HealthMonitorConfig configures the health monitor
type HealthMonitorConfig struct { 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) OnHealthChange func(healthy bool)
CheckInterval time.Duration
Timeout time.Duration
UnhealthyThreshold int
} }
// DefaultHealthMonitorConfig returns default health monitor configuration // DefaultHealthMonitorConfig returns default health monitor configuration
+5 -5
View File
@@ -15,8 +15,8 @@ import (
func TestRESPWriter_WriteCommand(t *testing.T) { func TestRESPWriter_WriteCommand(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args []string
expected string expected string
args []string
}{ }{
{ {
name: "Simple command", name: "Simple command",
@@ -205,9 +205,9 @@ func TestRESPReader_ReadInteger(t *testing.T) {
// TestRESPReader_ReadBulkString tests reading bulk strings // TestRESPReader_ReadBulkString tests reading bulk strings
func TestRESPReader_ReadBulkString(t *testing.T) { func TestRESPReader_ReadBulkString(t *testing.T) {
tests := []struct { tests := []struct {
expected interface{}
name string name string
input string input string
expected interface{}
wantErr bool wantErr bool
isNil bool isNil bool
}{ }{
@@ -440,10 +440,10 @@ func TestRESPHelpers(t *testing.T) {
// TestRESPRoundTrip tests full round-trip encoding/decoding // TestRESPRoundTrip tests full round-trip encoding/decoding
func TestRESPRoundTrip(t *testing.T) { func TestRESPRoundTrip(t *testing.T) {
tests := []struct { tests := []struct {
name string
command []string
response string
expected interface{} expected interface{}
name string
response string
command []string
}{ }{
{ {
name: "PING command", name: "PING command",
+28 -40
View File
@@ -33,21 +33,19 @@ type Logger interface {
// Config provides configuration for the cache // Config provides configuration for the cache
type Config struct { type Config struct {
Logger Logger
JWKConfig *JWKConfig
MetadataConfig *MetadataConfig
TokenConfig *TokenConfig
Type Type Type Type
MaxSize int
MaxMemoryBytes int64
DefaultTTL time.Duration DefaultTTL time.Duration
CleanupInterval time.Duration CleanupInterval time.Duration
EnableCompression bool MaxMemoryBytes int64
MaxSize int
EnableMetrics bool EnableMetrics bool
EnableAutoCleanup bool EnableAutoCleanup bool
EnableMemoryLimit bool EnableMemoryLimit bool
Logger Logger EnableCompression bool
// Type-specific configurations
TokenConfig *TokenConfig
MetadataConfig *MetadataConfig
JWKConfig *JWKConfig
} }
// TokenConfig provides token-specific cache configuration // TokenConfig provides token-specific cache configuration
@@ -59,11 +57,11 @@ type TokenConfig struct {
// MetadataConfig provides metadata-specific cache configuration // MetadataConfig provides metadata-specific cache configuration
type MetadataConfig struct { type MetadataConfig struct {
SecurityCriticalFields []string
GracePeriod time.Duration GracePeriod time.Duration
ExtendedGracePeriod time.Duration ExtendedGracePeriod time.Duration
MaxGracePeriod time.Duration MaxGracePeriod time.Duration
SecurityCriticalMaxGracePeriod time.Duration SecurityCriticalMaxGracePeriod time.Duration
SecurityCriticalFields []string
} }
// JWKConfig provides JWK-specific cache configuration // JWKConfig provides JWK-specific cache configuration
@@ -75,45 +73,35 @@ type JWKConfig struct {
// Item represents a single cache entry // Item represents a single cache entry
type Item struct { type Item struct {
Key string
Value interface{}
Size int64
ExpiresAt time.Time ExpiresAt time.Time
LastAccessed time.Time LastAccessed time.Time
AccessCount int64 Value interface{}
Metadata map[string]interface{}
element *list.Element
Key string
CacheType Type CacheType Type
Size int64
// Type-specific metadata AccessCount int64
Metadata map[string]interface{}
// LRU list element reference
element *list.Element
} }
// Cache provides a single, unified cache implementation // Cache provides a single, unified cache implementation
type Cache struct { type Cache struct {
mu sync.RWMutex config Config
items map[string]*Item ctx context.Context
lruList *list.List logger Logger
config Config cancel context.CancelFunc
logger Logger lruList *list.List
items map[string]*Item
// Memory management stopCleanup chan bool
wg sync.WaitGroup
currentSize int64 currentSize int64
currentMemory int64 currentMemory int64
hits int64
// Metrics misses int64
hits int64 evictions int64
misses int64 sets int64
evictions int64 mu sync.RWMutex
sets int64 closed int32
// Lifecycle management
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
stopCleanup chan bool
closed int32
} }
// DefaultConfig returns a default cache configuration // DefaultConfig returns a default cache configuration
+11 -11
View File
@@ -1750,19 +1750,19 @@ func TestAdvancedEdgeCases(t *testing.T) {
// Test with various data types // Test with various data types
testCases := []struct { testCases := []struct {
key string
value interface{} value interface{}
key string
}{ }{
{"string", "test string"}, {key: "string", value: "test string"},
{"int", 42}, {key: "int", value: 42},
{"float", 3.14159}, {key: "float", value: 3.14159},
{"bool", true}, {key: "bool", value: true},
{"slice", []string{"a", "b", "c"}}, {key: "slice", value: []string{"a", "b", "c"}},
{"map", map[string]int{"one": 1, "two": 2}}, {key: "map", value: map[string]int{"one": 1, "two": 2}},
{"nil", nil}, {key: "nil", value: nil},
{"empty-string", ""}, {key: "empty-string", value: ""},
{"empty-slice", []string{}}, {key: "empty-slice", value: []string{}},
{"empty-map", map[string]interface{}{}}, {key: "empty-map", value: map[string]interface{}{}},
} }
for _, tc := range testCases { for _, tc := range testCases {
+2 -7
View File
@@ -7,22 +7,17 @@ import (
// Manager manages multiple cache instances with singleton pattern // Manager manages multiple cache instances with singleton pattern
type Manager struct { type Manager struct {
mu sync.RWMutex logger Logger
// Core caches
tokenCache *Cache tokenCache *Cache
metadataCache *Cache metadataCache *Cache
jwkCache *Cache jwkCache *Cache
sessionCache *Cache sessionCache *Cache
generalCache *Cache generalCache *Cache
// Typed wrappers
typedToken *TokenCache typedToken *TokenCache
typedMetadata *MetadataCache typedMetadata *MetadataCache
typedJWK *JWKCache typedJWK *JWKCache
typedSession *SessionCache typedSession *SessionCache
mu sync.RWMutex
logger Logger
} }
var ( var (
+23 -42
View File
@@ -48,23 +48,12 @@ func (s State) String() string {
// CircuitBreakerConfig holds configuration for the circuit breaker // CircuitBreakerConfig holds configuration for the circuit breaker
type CircuitBreakerConfig struct { type CircuitBreakerConfig struct {
// MaxFailures is the number of consecutive failures before opening the circuit OnStateChange func(from, to State)
MaxFailures int MaxFailures int
FailureThreshold float64
// FailureThreshold is the failure rate threshold (0.0 to 1.0) Timeout time.Duration
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
HalfOpenMaxRequests int HalfOpenMaxRequests int
ResetTimeout time.Duration
// 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)
} }
// DefaultCircuitBreakerConfig returns default configuration // DefaultCircuitBreakerConfig returns default configuration
@@ -80,28 +69,20 @@ func DefaultCircuitBreakerConfig() *CircuitBreakerConfig {
// CircuitBreaker implements the circuit breaker pattern // CircuitBreaker implements the circuit breaker pattern
type CircuitBreaker struct { type CircuitBreaker struct {
config *CircuitBreakerConfig nextRetryTime time.Time
lastStateChange time.Time
// State management lastSuccessTime time.Time
state atomic.Int32 lastFailureTime time.Time
lastStateChange time.Time config *CircuitBreakerConfig
stateMu sync.RWMutex
// Failure tracking
consecutiveFailures atomic.Int32
totalRequests atomic.Int64
totalFailures atomic.Int64 totalFailures atomic.Int64
totalRequests atomic.Int64
stateTransitions atomic.Int64
rejectedRequests atomic.Int64
stateMu sync.RWMutex
timeMu sync.RWMutex
halfOpenRequests atomic.Int32 halfOpenRequests atomic.Int32
consecutiveFailures atomic.Int32
// Timing state atomic.Int32
lastFailureTime time.Time
lastSuccessTime time.Time
nextRetryTime time.Time
timeMu sync.RWMutex
// Metrics
stateTransitions atomic.Int64
rejectedRequests atomic.Int64
} }
// NewCircuitBreaker creates a new circuit breaker // NewCircuitBreaker creates a new circuit breaker
@@ -313,17 +294,17 @@ func (cb *CircuitBreaker) Stats() CircuitBreakerStats {
// CircuitBreakerStats holds statistics for the circuit breaker // CircuitBreakerStats holds statistics for the circuit breaker
type CircuitBreakerStats struct { type CircuitBreakerStats struct {
State State LastFailureTime time.Time
ConsecutiveFailures int32 LastSuccessTime time.Time
LastStateChange time.Time
NextRetryTime time.Time
TotalRequests int64 TotalRequests int64
TotalFailures int64 TotalFailures int64
SuccessRate float64 SuccessRate float64
RejectedRequests int64 RejectedRequests int64
StateTransitions int64 StateTransitions int64
LastFailureTime time.Time State State
LastSuccessTime time.Time ConsecutiveFailures int32
LastStateChange time.Time
NextRetryTime time.Time
} }
// IsHealthy returns true if the circuit breaker is in a healthy state // IsHealthy returns true if the circuit breaker is in a healthy state
+1 -1
View File
@@ -28,8 +28,8 @@ type mockBackend struct {
} }
type mockEntry struct { type mockEntry struct {
value []byte
expiresAt time.Time expiresAt time.Time
value []byte
} }
func newMockBackend() *mockBackend { func newMockBackend() *mockBackend {
+28 -49
View File
@@ -41,26 +41,13 @@ func (h HealthStatus) String() string {
// HealthCheckConfig holds configuration for the health checker // HealthCheckConfig holds configuration for the health checker
type HealthCheckConfig struct { type HealthCheckConfig struct {
// CheckInterval is how often to check health OnStatusChange func(from, to HealthStatus)
CheckInterval time.Duration CheckFunc func(ctx context.Context) error
CheckInterval time.Duration
// Timeout is the timeout for each health check Timeout time.Duration
Timeout time.Duration HealthyThreshold int
// HealthyThreshold is the number of consecutive successes to become healthy
HealthyThreshold int
// UnhealthyThreshold is the number of consecutive failures to become unhealthy
UnhealthyThreshold int UnhealthyThreshold int
DegradedThreshold time.Duration
// 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
} }
// DefaultHealthCheckConfig returns default configuration // DefaultHealthCheckConfig returns default configuration
@@ -76,31 +63,23 @@ func DefaultHealthCheckConfig() *HealthCheckConfig {
// HealthChecker monitors the health of a backend // HealthChecker monitors the health of a backend
type HealthChecker struct { type HealthChecker struct {
config *HealthCheckConfig lastCheckTime time.Time
lastSuccessTime time.Time
// Status tracking lastFailureTime time.Time
status atomic.Int32 config *HealthCheckConfig
consecutiveSuccesses atomic.Int32 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 consecutiveFailures atomic.Int32
consecutiveSuccesses atomic.Int32
// Timing stopped atomic.Bool
lastCheckTime time.Time status atomic.Int32
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
} }
// NewHealthChecker creates a new health checker // NewHealthChecker creates a new health checker
@@ -342,19 +321,19 @@ func (hc *HealthChecker) Stats() HealthCheckerStats {
// HealthCheckerStats holds statistics for the health checker // HealthCheckerStats holds statistics for the health checker
type HealthCheckerStats struct { type HealthCheckerStats struct {
Status HealthStatus LastCheckTime time.Time
ConsecutiveSuccesses int32 LastFailureTime time.Time
ConsecutiveFailures int32 LastSuccessTime time.Time
TotalChecks int64 TotalChecks int64
TotalSuccesses int64 TotalSuccesses int64
TotalFailures int64 TotalFailures int64
SuccessRate float64 SuccessRate float64
AverageLatency time.Duration AverageLatency time.Duration
StatusChanges int64 StatusChanges int64
LastCheckTime time.Time
LastSuccessTime time.Time
LastFailureTime time.Time
HealthScore float64 HealthScore float64
Status HealthStatus
ConsecutiveFailures int32
ConsecutiveSuccesses int32
} }
// Reset resets the health checker statistics // Reset resets the health checker statistics
+7 -11
View File
@@ -12,20 +12,16 @@ import (
// HealthCheckBackend wraps a cache backend with health checking // HealthCheckBackend wraps a cache backend with health checking
type HealthCheckBackend struct { type HealthCheckBackend struct {
backend backends.CacheBackend lastCheck time.Time
config *HealthCheckConfig backend backends.CacheBackend
ctx context.Context
// Health tracking config *HealthCheckConfig
cancel context.CancelFunc
wg sync.WaitGroup
checkMutex sync.RWMutex
status atomic.Int32 status atomic.Int32
consecutiveFails atomic.Int32 consecutiveFails atomic.Int32
consecutiveOK 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 // NewHealthCheckBackend creates a new health check wrapped backend
+2 -2
View File
@@ -292,12 +292,12 @@ type SessionCache struct {
// SessionData represents session information // SessionData represents session information
type SessionData struct { type SessionData struct {
ExpiresAt time.Time `json:"expires_at"`
Claims map[string]interface{} `json:"claims"`
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
Claims map[string]interface{} `json:"claims"`
} }
// NewSessionCache creates a new session cache // NewSessionCache creates a new session cache
+1 -1
View File
@@ -11,10 +11,10 @@ import (
// Mock logger for testing // Mock logger for testing
type mockLogger struct { type mockLogger struct {
mu sync.Mutex
logs []string logs []string
errLogs []string errLogs []string
debugLog []string debugLog []string
mu sync.Mutex
} }
func (m *mockLogger) Logf(format string, args ...interface{}) { func (m *mockLogger) Logf(format string, args ...interface{}) {
+11 -11
View File
@@ -19,20 +19,20 @@ type Logger interface {
// BackgroundTask represents a recurring background task // BackgroundTask represents a recurring background task
type BackgroundTask struct { type BackgroundTask struct {
name string lastRun time.Time
interval time.Duration logger Logger
taskFunc func() ctx context.Context
ticker *time.Ticker ticker *time.Ticker
stopChan chan bool stopChan chan bool
isRunning int32
logger Logger
waitGroup *sync.WaitGroup waitGroup *sync.WaitGroup
lastRun time.Time taskFunc func()
cancelFunc context.CancelFunc
name string
runCount int64 runCount int64
errorCount int64 errorCount int64
interval time.Duration
mu sync.RWMutex mu sync.RWMutex
ctx context.Context isRunning int32
cancelFunc context.CancelFunc
} }
// NewBackgroundTask creates a new background task // NewBackgroundTask creates a new background task
@@ -183,11 +183,11 @@ func (bt *BackgroundTask) IsRunning() bool {
// TaskRegistry manages all background tasks // TaskRegistry manages all background tasks
type TaskRegistry struct { type TaskRegistry struct {
tasks map[string]*BackgroundTask
mu sync.RWMutex
logger Logger logger Logger
maxTasks int tasks map[string]*BackgroundTask
circuitBreaker *TaskCircuitBreaker circuitBreaker *TaskCircuitBreaker
maxTasks int
mu sync.RWMutex
} }
// globalTaskRegistry is the singleton task registry // globalTaskRegistry is the singleton task registry
+13 -13
View File
@@ -11,14 +11,14 @@ import (
// TaskCircuitBreaker prevents task creation failures from cascading // TaskCircuitBreaker prevents task creation failures from cascading
type TaskCircuitBreaker struct { type TaskCircuitBreaker struct {
lastFailureTime time.Time
logger Logger
taskFailures map[string]int32
timeout time.Duration
mu sync.RWMutex
failureThreshold int32 failureThreshold int32
failureCount int32 failureCount int32
lastFailureTime time.Time state int32
timeout time.Duration
state int32 // 0: closed, 1: open
logger Logger
mu sync.RWMutex
taskFailures map[string]int32
} }
// CircuitBreakerState represents the state of the circuit breaker // 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 // TaskMemoryMonitor monitors memory usage and can trigger cleanup
type TaskMemoryMonitor struct { type TaskMemoryMonitor struct {
lastCheck time.Time
logger Logger logger Logger
registry *TaskRegistry registry *TaskRegistry
stopChan chan bool
memoryThreshold uint64 memoryThreshold uint64
checkInterval time.Duration checkInterval time.Duration
isMonitoring int32
stopChan chan bool
lastCheck time.Time
mu sync.RWMutex mu sync.RWMutex
isMonitoring int32
} }
var ( var (
@@ -310,13 +310,13 @@ func (tmm *TaskMemoryMonitor) GetStats() map[string]interface{} {
// WorkerPool manages a pool of worker goroutines for task execution // WorkerPool manages a pool of worker goroutines for task execution
type WorkerPool struct { type WorkerPool struct {
workers int
taskQueue chan func()
workerWg sync.WaitGroup
isRunning int32
logger Logger logger Logger
taskQueue chan func()
stopChan chan bool stopChan chan bool
metrics WorkerPoolMetrics metrics WorkerPoolMetrics
workerWg sync.WaitGroup
workers int
isRunning int32
} }
// WorkerPoolMetrics tracks worker pool performance // WorkerPoolMetrics tracks worker pool performance
+2 -2
View File
@@ -12,9 +12,9 @@ import (
type FeatureFlag struct { type FeatureFlag struct {
name string name string
description string description string
enabled atomic.Bool
mu sync.RWMutex
callbacks []func(bool) callbacks []func(bool)
mu sync.RWMutex
enabled atomic.Bool
} }
// FeatureManager manages all feature flags in the application // FeatureManager manages all feature flags in the application
+19 -28
View File
@@ -14,50 +14,41 @@ import (
// and resource leaks. It provides centralized management of HTTP client transports with // and resource leaks. It provides centralized management of HTTP client transports with
// proper lifecycle management and security controls. // proper lifecycle management and security controls.
type TransportPool struct { type TransportPool struct {
mu sync.RWMutex
transports map[string]*sharedTransport
maxConns int
ctx context.Context ctx context.Context
transports map[string]*sharedTransport
cancel context.CancelFunc cancel context.CancelFunc
clientCount int32 // Track total HTTP clients maxConns int
maxClients int32 // Limit total clients mu sync.RWMutex
clientCount int32
maxClients int32
} }
// sharedTransport wraps an HTTP transport with reference counting // sharedTransport wraps an HTTP transport with reference counting
type sharedTransport struct { type sharedTransport struct {
transport *http.Transport
refCount int32
lastUsed time.Time lastUsed time.Time
transport *http.Transport
config TransportConfig config TransportConfig
refCount int32
} }
// TransportConfig defines configuration for HTTP transports // TransportConfig defines configuration for HTTP transports
type TransportConfig struct { type TransportConfig struct {
// Timeouts MaxConnsPerHost int
DialTimeout time.Duration WriteBufferSize int
TLSHandshakeTimeout time.Duration
ResponseHeaderTimeout time.Duration ResponseHeaderTimeout time.Duration
ExpectContinueTimeout time.Duration ExpectContinueTimeout time.Duration
IdleConnTimeout time.Duration IdleConnTimeout time.Duration
KeepAlive time.Duration KeepAlive time.Duration
TLSHandshakeTimeout time.Duration
// Connection limits MaxIdleConns int
MaxIdleConns int DialTimeout time.Duration
MaxIdleConnsPerHost int MaxIdleConnsPerHost int
MaxConnsPerHost int ReadBufferSize int
MinTLSVersion uint16
// Features ForceHTTP2 bool
ForceHTTP2 bool DisableCompression bool
DisableKeepAlives bool InsecureSkipVerify bool
DisableCompression bool DisableKeepAlives bool
// Buffer sizes
WriteBufferSize int
ReadBufferSize int
// TLS
InsecureSkipVerify bool
MinTLSVersion uint16
} }
var ( var (
+4 -4
View File
@@ -154,10 +154,10 @@ func TestAzureProvider_ValidateTokens(t *testing.T) {
provider := NewAzureProvider() provider := NewAzureProvider()
tests := []struct { tests := []struct {
name string
session *mockSession
verifierError error verifierError error
session *mockSession
cacheData map[string]interface{} cacheData map[string]interface{}
name string
expectedResult ValidationResult expectedResult ValidationResult
}{ }{
{ {
@@ -369,9 +369,9 @@ func TestAzureProvider_OfflineAccessHandling(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
inputScopes []string
expectedCount int // Expected number of offline_access scopes (should be 1)
description string description string
inputScopes []string
expectedCount int
}{ }{
{ {
name: "No offline_access - should add one", name: "No offline_access - should add one",
+5 -5
View File
@@ -8,10 +8,10 @@ import (
// Mock implementations for testing // Mock implementations for testing
type mockSession struct { type mockSession struct {
authenticated bool
idToken string idToken string
accessToken string accessToken string
refreshToken string refreshToken string
authenticated bool
} }
func (s *mockSession) GetIDToken() string { return s.idToken } func (s *mockSession) GetIDToken() string { return s.idToken }
@@ -338,10 +338,10 @@ func TestBaseProvider_ValidateTokenExpiry(t *testing.T) {
gracePeriod := 5 * time.Minute gracePeriod := 5 * time.Minute
tests := []struct { tests := []struct {
name string
claims map[string]interface{} claims map[string]interface{}
cacheFound bool name string
expectedResult ValidationResult expectedResult ValidationResult
cacheFound bool
}{ }{
{ {
name: "Token not found in cache, has refresh token", name: "Token not found in cache, has refresh token",
@@ -438,10 +438,10 @@ func TestBaseProvider_ValidateTokenExpiry_NoRefreshToken(t *testing.T) {
gracePeriod := 5 * time.Minute gracePeriod := 5 * time.Minute
tests := []struct { tests := []struct {
name string
claims map[string]interface{} claims map[string]interface{}
cacheFound bool name string
expectedResult ValidationResult expectedResult ValidationResult
cacheFound bool
}{ }{
{ {
name: "Token not found in cache, no refresh token", name: "Token not found in cache, no refresh token",
+2 -2
View File
@@ -25,9 +25,9 @@ func TestProviderFactory_CreateProvider(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
issuerURL string issuerURL string
errMsg string
expectedType ProviderType expectedType ProviderType
wantErr bool wantErr bool
errMsg string
}{ }{
{ {
name: "Google provider", name: "Google provider",
@@ -158,10 +158,10 @@ func TestProviderFactory_CreateProviderByType(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
errMsg string
providerType ProviderType providerType ProviderType
expectedType ProviderType expectedType ProviderType
wantErr bool wantErr bool
errMsg string
}{ }{
{ {
name: "Generic provider", name: "Generic provider",
+2 -2
View File
@@ -136,9 +136,9 @@ func TestGenericProvider_ValidateTokens(t *testing.T) {
provider := NewGenericProvider() provider := NewGenericProvider()
tests := []struct { tests := []struct {
name string
session *mockSession
verifierError error verifierError error
session *mockSession
name string
expectedResult ValidationResult expectedResult ValidationResult
}{ }{
{ {
+1 -1
View File
@@ -172,8 +172,8 @@ func TestGoogleProvider_OfflineAccessFiltering(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
inputScopes []string
description string description string
inputScopes []string
}{ }{
{ {
name: "Multiple offline_access occurrences", name: "Multiple offline_access occurrences",
+3 -3
View File
@@ -82,9 +82,9 @@ func TestProviderRegistry_GetProviderByType(t *testing.T) {
registry.RegisterProvider(googleProvider) registry.RegisterProvider(googleProvider)
tests := []struct { tests := []struct {
expected OIDCProvider
name string name string
providerType ProviderType providerType ProviderType
expected OIDCProvider
}{ }{
{ {
name: "Get Generic provider", name: "Get Generic provider",
@@ -180,9 +180,9 @@ func TestProviderRegistry_DetectProvider(t *testing.T) {
registry.RegisterProvider(gitlabProvider) registry.RegisterProvider(gitlabProvider)
tests := []struct { tests := []struct {
expected OIDCProvider
name string name string
issuerURL string issuerURL string
expected OIDCProvider
}{ }{
{ {
name: "Google provider detection", name: "Google provider detection",
@@ -640,9 +640,9 @@ func TestProviderRegistry_GitLabDetection_RealWorldURLs(t *testing.T) {
registry.RegisterProvider(githubProvider) registry.RegisterProvider(githubProvider)
realWorldTests := []struct { realWorldTests := []struct {
expected OIDCProvider
name string name string
issuerURL string issuerURL string
expected OIDCProvider
}{ }{
// Actual self-hosted GitLab examples from issue #61 // Actual self-hosted GitLab examples from issue #61
{ {
+9 -9
View File
@@ -20,8 +20,8 @@ func TestValidateIssuerURL(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
issuerURL string issuerURL string
wantErr bool
errMsg string errMsg string
wantErr bool
}{ }{
{ {
name: "valid https URL", name: "valid https URL",
@@ -106,8 +106,8 @@ func TestValidateClientID(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
clientID string clientID string
wantErr bool
errMsg string errMsg string
wantErr bool
}{ }{
{ {
name: "valid client ID", name: "valid client ID",
@@ -173,9 +173,9 @@ func TestValidateClientID(t *testing.T) {
func TestValidateScopes(t *testing.T) { func TestValidateScopes(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
errMsg string
scopes []string scopes []string
wantErr bool wantErr bool
errMsg string
}{ }{
{ {
name: "valid scopes with openid", name: "valid scopes with openid",
@@ -248,8 +248,8 @@ func TestValidateRedirectURL(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
redirectURL string redirectURL string
wantErr bool
errMsg string errMsg string
wantErr bool
}{ }{
{ {
name: "valid https redirect URL", name: "valid https redirect URL",
@@ -315,11 +315,11 @@ func TestValidateRedirectURL(t *testing.T) {
// TestValidateProviderSpecificConfig tests provider-specific configuration validation // TestValidateProviderSpecificConfig tests provider-specific configuration validation
func TestValidateProviderSpecificConfig(t *testing.T) { func TestValidateProviderSpecificConfig(t *testing.T) {
tests := []struct { tests := []struct {
name string
provider OIDCProvider provider OIDCProvider
config map[string]interface{} config map[string]interface{}
wantErr bool name string
errMsg string errMsg string
wantErr bool
}{ }{
{ {
name: "valid Google config", name: "valid Google config",
@@ -458,8 +458,8 @@ func TestValidateGoogleConfig_EdgeCases(t *testing.T) {
googleProvider := NewGoogleProvider() googleProvider := NewGoogleProvider()
tests := []struct { tests := []struct {
name string
config map[string]interface{} config map[string]interface{}
name string
wantErr bool wantErr bool
}{ }{
{ {
@@ -502,10 +502,10 @@ func TestValidateAzureConfig_EdgeCases(t *testing.T) {
azureProvider := NewAzureProvider() azureProvider := NewAzureProvider()
tests := []struct { tests := []struct {
name string
config map[string]interface{} config map[string]interface{}
wantErr bool name string
errMsg string errMsg string
wantErr bool
}{ }{
{ {
name: "valid tenant ID format", name: "valid tenant ID format",
+2 -2
View File
@@ -7,9 +7,9 @@ import (
// ProviderWarning represents a warning about provider limitations or requirements. // ProviderWarning represents a warning about provider limitations or requirements.
type ProviderWarning struct { type ProviderWarning struct {
ProviderType ProviderType Level string
Level string // "info", "warning", "error"
Message string Message string
ProviderType ProviderType
} }
// GetProviderWarnings returns warnings about provider-specific limitations. // GetProviderWarnings returns warnings about provider-specific limitations.
+1 -1
View File
@@ -9,9 +9,9 @@ import (
func TestGetProviderWarnings(t *testing.T) { func TestGetProviderWarnings(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
checkContent string
providerType ProviderType providerType ProviderType
expectCount int expectCount int
checkContent string
}{ }{
{ {
name: "GitHub has OAuth 2.0 warning", name: "GitHub has OAuth 2.0 warning",
+8 -14
View File
@@ -34,21 +34,15 @@ type Logger interface {
// for all recovery mechanism implementations. It handles request counting, // for all recovery mechanism implementations. It handles request counting,
// success/failure tracking, and timestamp management in a thread-safe manner. // success/failure tracking, and timestamp management in a thread-safe manner.
type BaseRecoveryMechanism struct { type BaseRecoveryMechanism struct {
// name identifies the recovery mechanism instance logger Logger
name string name string
// logger provides structured logging capabilities lastSuccessStr string
logger Logger lastFailureStr string
// Metrics tracked with atomic operations for thread safety
totalRequests int64 totalRequests int64
successCount int64 successCount int64
failureCount int64 failureCount int64
lastSuccessStr string successMutex sync.RWMutex
lastFailureStr string failureMutex sync.RWMutex
// mutexes for thread-safe timestamp updates
successMutex sync.RWMutex
failureMutex sync.RWMutex
} }
// NewBaseRecoveryMechanism creates a new base recovery mechanism with the given name and logger. // 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 // HTTPError represents an HTTP error with status code and message
type HTTPError struct { type HTTPError struct {
StatusCode int Headers map[string]string
Message string Message string
Body []byte Body []byte
Headers map[string]string StatusCode int
} }
// Error implements the error interface // Error implements the error interface
+7 -13
View File
@@ -60,20 +60,14 @@ func DefaultCircuitBreakerConfig() CircuitBreakerConfig {
// CircuitBreaker implements the circuit breaker pattern for fault tolerance. // CircuitBreaker implements the circuit breaker pattern for fault tolerance.
// It prevents cascading failures by temporarily blocking requests to a failing service. // It prevents cascading failures by temporarily blocking requests to a failing service.
type CircuitBreaker struct { type CircuitBreaker struct {
*BaseRecoveryMechanism
config CircuitBreakerConfig
// State management
state int32 // atomic: CircuitBreakerState
lastStateChange time.Time lastStateChange time.Time
stateMutex sync.RWMutex *BaseRecoveryMechanism
config CircuitBreakerConfig
// Failure tracking stateMutex sync.RWMutex
consecutiveFailures int32 // atomic state int32
consecutiveSuccesses int32 // atomic consecutiveFailures int32
consecutiveSuccesses int32
// Half-open state management halfOpenRequests int32
halfOpenRequests int32 // atomic
} }
// NewCircuitBreaker creates a new circuit breaker with the given configuration // NewCircuitBreaker creates a new circuit breaker with the given configuration
+8 -17
View File
@@ -15,20 +15,13 @@ import (
// RetryConfig defines configuration for the retry executor // RetryConfig defines configuration for the retry executor
type RetryConfig struct { type RetryConfig struct {
// MaxAttempts is the maximum number of retry attempts RetryableErrors []string
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
RetryableStatusCodes []int RetryableStatusCodes []int
MaxAttempts int
InitialDelay time.Duration
MaxDelay time.Duration
Multiplier float64
RandomizationFactor float64
} }
// DefaultRetryConfig returns sensible default retry configuration // DefaultRetryConfig returns sensible default retry configuration
@@ -46,13 +39,11 @@ func DefaultRetryConfig() RetryConfig {
// RetryExecutor implements retry logic with exponential backoff // RetryExecutor implements retry logic with exponential backoff
type RetryExecutor struct { type RetryExecutor struct {
lastRetryTime time.Time
*BaseRecoveryMechanism *BaseRecoveryMechanism
config RetryConfig config RetryConfig
// Metrics
totalRetries int64 totalRetries int64
maxRetriesHit int64 maxRetriesHit int64
lastRetryTime time.Time
retryTimeMutex sync.RWMutex retryTimeMutex sync.RWMutex
} }
+8 -8
View File
@@ -273,17 +273,17 @@ func TestRetryExecutor_isRetryableError(t *testing.T) {
executor := NewRetryExecutor(config, logger) executor := NewRetryExecutor(config, logger)
tests := []struct { tests := []struct {
name string
err error err error
name string
expected bool expected bool
}{ }{
{"nil error", nil, false}, {name: "nil error", err: nil, expected: false},
{"connection refused", errors.New("connection refused"), true}, {name: "connection refused", err: errors.New("connection refused"), expected: true},
{"timeout", errors.New("TIMEOUT"), true}, // case insensitive {name: "timeout", err: errors.New("TIMEOUT"), expected: true}, // case insensitive
{"EOF", errors.New("EOF"), false}, {name: "EOF", err: errors.New("EOF"), expected: false},
{"random error", errors.New("something else"), false}, {name: "random error", err: errors.New("something else"), expected: false},
{"context cancelled", context.Canceled, false}, {name: "context cancelled", err: context.Canceled, expected: false},
{"context deadline exceeded", context.DeadlineExceeded, false}, {name: "context deadline exceeded", err: context.DeadlineExceeded, expected: false},
} }
for _, tt := range tests { for _, tt := range tests {
+6 -6
View File
@@ -13,10 +13,10 @@ import (
// Mock logger for testing // Mock logger for testing
type mockLogger struct { type mockLogger struct {
mu sync.Mutex
logs []string logs []string
errLogs []string errLogs []string
debugLog []string debugLog []string
mu sync.Mutex
} }
func (m *mockLogger) Logf(format string, args ...interface{}) { func (m *mockLogger) Logf(format string, args ...interface{}) {
@@ -202,13 +202,13 @@ func TestBaseRecoveryMechanism_ConcurrentAccess(t *testing.T) {
// CircuitBreakerState tests // CircuitBreakerState tests
func TestCircuitBreakerState_String(t *testing.T) { func TestCircuitBreakerState_String(t *testing.T) {
tests := []struct { tests := []struct {
state CircuitBreakerState
expected string expected string
state CircuitBreakerState
}{ }{
{CircuitBreakerClosed, "closed"}, {state: CircuitBreakerClosed, expected: "closed"},
{CircuitBreakerOpen, "open"}, {state: CircuitBreakerOpen, expected: "open"},
{CircuitBreakerHalfOpen, "half-open"}, {state: CircuitBreakerHalfOpen, expected: "half-open"},
{CircuitBreakerState(99), "unknown"}, {state: CircuitBreakerState(99), expected: "unknown"},
} }
for _, tt := range tests { for _, tt := range tests {
+5 -5
View File
@@ -29,26 +29,26 @@ type JWK struct {
type TokenResponse struct { type TokenResponse struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
TokenType string `json:"token_type"` TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token,omitempty"` IDToken string `json:"id_token,omitempty"`
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
ExpiresIn int `json:"expires_in"`
} }
// IntrospectionResponse represents a token introspection response // IntrospectionResponse represents a token introspection response
type IntrospectionResponse struct { type IntrospectionResponse struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"` ClientID string `json:"client_id,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
TokenType string `json:"token_type,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"` Sub string `json:"sub,omitempty"`
Aud string `json:"aud,omitempty"` Aud string `json:"aud,omitempty"`
Iss string `json:"iss,omitempty"` Iss string `json:"iss,omitempty"`
Jti string `json:"jti,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 // JWKCache is a testify mock for JWK caching operations
+2 -2
View File
@@ -8,16 +8,16 @@ import (
// SessionData represents session data for testing // SessionData represents session data for testing
type SessionData struct { type SessionData struct {
Claims map[string]interface{}
Email string Email string
AccessToken string AccessToken string
RefreshToken string RefreshToken string
IDToken string IDToken string
Expiry int64
Nonce string Nonce string
State string State string
CodeVerifier string CodeVerifier string
RedirectURL string RedirectURL string
Claims map[string]interface{} Expiry int64
} }
// SessionManager is a testify mock for session management // SessionManager is a testify mock for session management
+24 -39
View File
@@ -16,45 +16,30 @@ import (
// OIDCServerConfig configures the mock OIDC server behavior // OIDCServerConfig configures the mock OIDC server behavior
type OIDCServerConfig struct { type OIDCServerConfig struct {
// Identity JWKSResponse map[string]interface{}
Issuer string TokenFixture *fixtures.TokenFixture
UserinfoError *OIDCError
// Discovery UserinfoResponse map[string]interface{}
ScopesSupported []string IntrospectionResponse map[string]interface{}
ResponseTypesSupported []string JWKSError *OIDCError
RefreshError *OIDCError
TokenResponse map[string]interface{}
TokenError *OIDCError
IntrospectionError *OIDCError
RefreshResponse map[string]interface{}
Issuer string
GrantTypesSupported []string GrantTypesSupported []string
ClaimsSupported []string
TokenEndpointAuthMethods []string TokenEndpointAuthMethods []string
ScopesSupported []string
// Token fixture for signing ClaimsSupported []string
TokenFixture *fixtures.TokenFixture ResponseTypesSupported []string
FailAfterN int
// Token endpoint behavior JWKSDelay time.Duration
TokenResponse map[string]interface{} TimeoutDuration time.Duration
TokenError *OIDCError RateLimitAfter int
TokenDelay time.Duration TokenDelay time.Duration
RefreshResponse map[string]interface{} FailWithStatus int
RefreshError *OIDCError SimulateTimeout bool
// 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
} }
// OIDCError represents an OAuth error response // OIDCError represents an OAuth error response
@@ -67,9 +52,9 @@ type OIDCError struct {
type OIDCServer struct { type OIDCServer struct {
*httptest.Server *httptest.Server
Config *OIDCServerConfig Config *OIDCServerConfig
RequestCount int32
mu sync.Mutex
requests []*http.Request requests []*http.Request
mu sync.Mutex
RequestCount int32
} }
// NewOIDCServer creates a new mock OIDC server // NewOIDCServer creates a new mock OIDC server
+4 -4
View File
@@ -135,9 +135,9 @@ func TestIsTestMode(t *testing.T) {
// We'll test what we can control via environment variables. // We'll test what we can control via environment variables.
tests := []struct { tests := []struct {
name string
setup func() setup func()
cleanup func() cleanup func()
name string
expected bool expected bool
}{ }{
{ {
@@ -206,8 +206,8 @@ func TestIsTestMode(t *testing.T) {
func TestIsTestModeEdgeCases(t *testing.T) { func TestIsTestModeEdgeCases(t *testing.T) {
// Test with various environment variable combinations // Test with various environment variable combinations
tests := []struct { tests := []struct {
name string
env map[string]string env map[string]string
name string
}{ }{
{ {
name: "all env vars empty", name: "all env vars empty",
@@ -560,11 +560,11 @@ func TestIsTestModeYaegiCompiler(t *testing.T) {
// mockLogger is a simple mock implementation for testing // mockLogger is a simple mock implementation for testing
type mockLogger struct { type mockLogger struct {
lastFormat string
lastArgs []interface{}
infoCalls int infoCalls int
debugCalls int debugCalls int
errorCalls int errorCalls int
lastFormat string
lastArgs []interface{}
} }
func (m *mockLogger) Infof(format string, args ...interface{}) { func (m *mockLogger) Infof(format string, args ...interface{}) {
+9 -18
View File
@@ -21,25 +21,16 @@ import (
// JWK represents a JSON Web Key as defined in RFC 7517. // JWK represents a JSON Web Key as defined in RFC 7517.
// It can represent different key types including RSA, EC, and symmetric keys. // It can represent different key types including RSA, EC, and symmetric keys.
type JWK struct { type JWK struct {
// Key type (e.g., "RSA", "EC", "oct") Kty string `json:"kty"`
Kty string `json:"kty"` Use string `json:"use,omitempty"`
// Key use (e.g., "sig" for signature, "enc" for encryption) Alg string `json:"alg,omitempty"`
Use string `json:"use,omitempty"` Kid string `json:"kid,omitempty"`
// Key operations allowed 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"` 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. // JWKSet represents a set of JSON Web Keys.
+9 -9
View File
@@ -309,18 +309,18 @@ func TestJWKCacheCleanupAndClose(t *testing.T) {
func TestFetchJWKSEdgeCases(t *testing.T) { func TestFetchJWKSEdgeCases(t *testing.T) {
t.Run("handles various HTTP status codes", func(t *testing.T) { t.Run("handles various HTTP status codes", func(t *testing.T) {
testCases := []struct { testCases := []struct {
errContains string
status int status int
wantErr bool wantErr bool
errContains string
}{ }{
{200, false, ""}, {status: 200, wantErr: false, errContains: ""},
{400, true, "400"}, {status: 400, wantErr: true, errContains: "400"},
{401, true, "401"}, {status: 401, wantErr: true, errContains: "401"},
{403, true, "403"}, {status: 403, wantErr: true, errContains: "403"},
{404, true, "404"}, {status: 404, wantErr: true, errContains: "404"},
{500, true, "500"}, {status: 500, wantErr: true, errContains: "500"},
{502, true, "502"}, {status: 502, wantErr: true, errContains: "502"},
{503, true, "503"}, {status: 503, wantErr: true, errContains: "503"},
} }
for _, tc := range testCases { for _, tc := range testCases {
+3 -3
View File
@@ -18,15 +18,15 @@ import (
// TestExchangeCodeForToken_Comprehensive tests the ExchangeCodeForToken function comprehensively // TestExchangeCodeForToken_Comprehensive tests the ExchangeCodeForToken function comprehensively
func TestExchangeCodeForToken_Comprehensive(t *testing.T) { func TestExchangeCodeForToken_Comprehensive(t *testing.T) {
tests := []struct { tests := []struct {
setupMock func(*httptest.Server) *TraefikOidc
validateFunc func(*testing.T, *TokenResponse, error)
name string name string
grantType string grantType string
code string code string
redirectURL string redirectURL string
codeVerifier string codeVerifier string
setupMock func(*httptest.Server) *TraefikOidc
validateFunc func(*testing.T, *TokenResponse, error)
wantErr bool
expectedError string expectedError string
wantErr bool
}{ }{
{ {
name: "successful authorization code exchange", name: "successful authorization code exchange",
+2 -2
View File
@@ -13,9 +13,9 @@ import (
func TestGoroutineLeakPrevention_ContextCancellation(t *testing.T) { func TestGoroutineLeakPrevention_ContextCancellation(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
cancelAfter time.Duration
expectedLeaks int // Maximum expected goroutines after cleanup
description string description string
cancelAfter time.Duration
expectedLeaks int
}{ }{
{ {
name: "immediate_cancellation", name: "immediate_cancellation",
+2 -2
View File
@@ -15,10 +15,10 @@ import (
// TestInitializeMetadata tests the initializeMetadata function // TestInitializeMetadata tests the initializeMetadata function
func TestInitializeMetadata(t *testing.T) { func TestInitializeMetadata(t *testing.T) {
tests := []struct { tests := []struct {
name string
providerURL string
setupMock func() *httptest.Server setupMock func() *httptest.Server
validateFunc func(*testing.T, *TraefikOidc) validateFunc func(*testing.T, *TraefikOidc)
name string
providerURL string
wantPanic bool wantPanic bool
}{ }{
{ {
+3 -3
View File
@@ -16,12 +16,12 @@ import (
// TestGetNewTokenWithRefreshToken tests the GetNewTokenWithRefreshToken function // TestGetNewTokenWithRefreshToken tests the GetNewTokenWithRefreshToken function
func TestGetNewTokenWithRefreshToken(t *testing.T) { func TestGetNewTokenWithRefreshToken(t *testing.T) {
tests := []struct { tests := []struct {
name string
refreshToken string
setupMock func(*httptest.Server) *TraefikOidc setupMock func(*httptest.Server) *TraefikOidc
validateFunc func(*testing.T, *TokenResponse, error) validateFunc func(*testing.T, *TokenResponse, error)
wantErr bool name string
refreshToken string
expectedError string expectedError string
wantErr bool
}{ }{
{ {
name: "successful token refresh", name: "successful token refresh",
+4 -4
View File
@@ -10,9 +10,9 @@ import (
// TestServeHTTP_ExcludedURLs tests the excluded URLs functionality // TestServeHTTP_ExcludedURLs tests the excluded URLs functionality
func TestServeHTTP_ExcludedURLs(t *testing.T) { func TestServeHTTP_ExcludedURLs(t *testing.T) {
tests := []struct { tests := []struct {
excludedURLs map[string]struct{}
name string name string
path string path string
excludedURLs map[string]struct{}
shouldBypass bool shouldBypass bool
}{ }{
{ {
@@ -506,12 +506,12 @@ type MockSessionData struct {
idToken string idToken string
accessToken string accessToken string
refreshToken string refreshToken string
authenticated bool
isDirty bool
redirectCount int
csrf string csrf string
nonce string nonce string
codeVerifier string codeVerifier string
redirectCount int
authenticated bool
isDirty bool
} }
func (m *MockSessionData) GetEmail() string { return m.email } func (m *MockSessionData) GetEmail() string { return m.email }
+2 -2
View File
@@ -81,11 +81,11 @@ func TestIsTestMode_DefaultBehavior(t *testing.T) {
// TestVerifyAudience tests the verifyAudience function // TestVerifyAudience tests the verifyAudience function
func TestVerifyAudience(t *testing.T) { func TestVerifyAudience(t *testing.T) {
tests := []struct { tests := []struct {
name string
tokenAudience interface{} tokenAudience interface{}
name string
expectedAudience string expectedAudience string
expectError bool
description string description string
expectError bool
}{ }{
{ {
name: "Audience matches", name: "Audience matches",
+2 -2
View File
@@ -192,9 +192,9 @@ func (ts *TestSuite) Setup() {
// MockJWKCache implements JWKCacheInterface // MockJWKCache implements JWKCacheInterface
type MockJWKCache struct { type MockJWKCache struct {
mu sync.RWMutex
JWKS *JWKSet
Err error Err error
JWKS *JWKSet
mu sync.RWMutex
} }
// Close is a no-op for the mock. // Close is a no-op for the mock.
+11 -11
View File
@@ -42,27 +42,27 @@ func NewMemoryLeakFixesTestSuite() *MemoryLeakFixesTestSuite {
// MemoryTestCase defines a memory leak test scenario // MemoryTestCase defines a memory leak test scenario
type MemoryTestCase struct { 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 setup func(*MemoryTestFramework) error
execute func(*MemoryTestFramework) error execute func(*MemoryTestFramework) error
validateLeak func(*testing.T, runtime.MemStats, runtime.MemStats) validateLeak func(*testing.T, runtime.MemStats, runtime.MemStats)
cleanup func(*MemoryTestFramework) error cleanup func(*MemoryTestFramework) error
name string
component string
scenario string
iterations int
concurrency int
} }
// MemoryTestFramework provides common test infrastructure for memory tests // MemoryTestFramework provides common test infrastructure for memory tests
type MemoryTestFramework struct { type MemoryTestFramework struct {
t *testing.T
cache CacheInterface cache CacheInterface
ctx context.Context
t *testing.T
plugin *TraefikOidc plugin *TraefikOidc
logger *Logger logger *Logger
cancel context.CancelFunc
servers []*httptest.Server servers []*httptest.Server
configs []*Config configs []*Config
ctx context.Context
cancel context.CancelFunc
} }
// NewMemoryTestFramework creates a new test framework instance // NewMemoryTestFramework creates a new test framework instance
@@ -97,12 +97,12 @@ func (tf *MemoryTestFramework) Cleanup() {
// ConsolidatedMemorySnapshot captures memory statistics at a point in time // ConsolidatedMemorySnapshot captures memory statistics at a point in time
type ConsolidatedMemorySnapshot struct { type ConsolidatedMemorySnapshot struct {
Timestamp time.Time Timestamp time.Time
Description string
Alloc uint64 Alloc uint64
TotalAlloc uint64 TotalAlloc uint64
Sys uint64 Sys uint64
NumGC uint32
Goroutines int Goroutines int
Description string NumGC uint32
} }
// VerifyNoGoroutineLeaks checks for goroutine leaks // VerifyNoGoroutineLeaks checks for goroutine leaks
@@ -1601,8 +1601,8 @@ func TestMemoryLeakConsolidated(t *testing.T) {
func TestGoroutineLeaks(t *testing.T) { func TestGoroutineLeaks(t *testing.T) {
testCases := []struct { testCases := []struct {
name string
test func(t *testing.T) test func(t *testing.T)
name string
}{ }{
{ {
name: "cache_no_leak", name: "cache_no_leak",
+28 -38
View File
@@ -10,30 +10,24 @@ import (
// MemoryStats holds comprehensive memory statistics // MemoryStats holds comprehensive memory statistics
type MemoryStats struct { type MemoryStats struct {
// Go runtime memory stats LastGCTime time.Time
HeapAllocBytes uint64 // bytes allocated and still in use Timestamp time.Time
HeapSysBytes uint64 // bytes obtained from system GCSysBytes uint64
HeapIdleBytes uint64 // bytes in idle (unused) spans NumGoroutines int
HeapInuseBytes uint64 // bytes in in-use spans HeapReleasedBytes uint64
HeapReleasedBytes uint64 // bytes released to the OS HeapObjects uint64
HeapObjects uint64 // total number of allocated objects StackInuseBytes uint64
StackInuseBytes uint64 // bytes in stack spans StackSysBytes uint64
StackSysBytes uint64 // bytes obtained from system for stack HeapAllocBytes uint64
GCSysBytes uint64 // bytes used for garbage collection system metadata HeapInuseBytes uint64
NumGoroutines int // number of goroutines that currently exist HeapIdleBytes uint64
LastGCTime time.Time // time of last garbage collection SessionCount int
TaskCount int
// Application-specific memory tracking CacheSize int64
SessionCount int // current number of sessions ConnectionPools int
TaskCount int // current number of background tasks MemoryPressure MemoryPressureLevel
CacheSize int64 // estimated cache memory usage GCFrequency float64
ConnectionPools int // number of HTTP connection pools HeapSysBytes uint64
// Memory pressure indicators
MemoryPressure MemoryPressureLevel // overall memory pressure level
GCFrequency float64 // garbage collections per minute
Timestamp time.Time
} }
// MemoryPressureLevel indicates the current memory pressure // MemoryPressureLevel indicates the current memory pressure
@@ -66,22 +60,18 @@ func (mpl MemoryPressureLevel) String() string {
// MemoryMonitor provides comprehensive memory monitoring and alerting // MemoryMonitor provides comprehensive memory monitoring and alerting
type MemoryMonitor struct { type MemoryMonitor struct {
logger *Logger lastGCTime time.Time
mu sync.RWMutex startTime time.Time
lastStats *MemoryStats lastStats *MemoryStats
lastGCCount uint32 logger *Logger
lastGCTime time.Time alertThresholds MemoryAlertThresholds
startTime time.Time
alertThresholds MemoryAlertThresholds
// Memory leak detection
baselineHeap uint64
heapGrowthRate float64 // bytes per second
suspiciousGrowth bool
// Goroutine tracking
baselineGoroutines int baselineGoroutines int
baselineHeap uint64
heapGrowthRate float64
maxGoroutines int64 maxGoroutines int64
mu sync.RWMutex
lastGCCount uint32
suspiciousGrowth bool
goroutineLeakAlert bool goroutineLeakAlert bool
} }
+33 -52
View File
@@ -19,31 +19,25 @@ import (
// MockOAuthProvider simulates an OAuth/OIDC provider for testing // MockOAuthProvider simulates an OAuth/OIDC provider for testing
type MockOAuthProvider struct { type MockOAuthProvider struct {
TokenEndpoint string TokenExchangeFunc func(grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error)
AuthEndpoint string LastRequest *http.Request
JWKSEndpoint string JWKSResponseFunc func() ([]byte, error)
RevokeEndpoint string RevokeTokenFunc func(token, tokenType string) error
EndSessionEndpoint string RefreshTokenFunc func(refreshToken string) (*TokenResponse, error)
EndSessionEndpoint string
// Configurable behaviors TokenEndpoint string
TokenExchangeFunc func(grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) RevokeEndpoint string
RefreshTokenFunc func(refreshToken string) (*TokenResponse, error) JWKSEndpoint string
RevokeTokenFunc func(token, tokenType string) error AuthEndpoint string
JWKSResponseFunc func() ([]byte, error) RequestHistory []*http.Request
LastRequestBody []byte
// Simulation flags
SimulateTimeout bool
SimulateRateLimit bool
SimulateServerError bool
TimeoutDuration time.Duration TimeoutDuration time.Duration
ResponseDelay time.Duration ResponseDelay time.Duration
mu sync.Mutex
// Request tracking RequestCount int32
RequestCount int32 SimulateServerError bool
LastRequest *http.Request SimulateRateLimit bool
LastRequestBody []byte SimulateTimeout bool
RequestHistory []*http.Request
mu sync.Mutex
} }
// NewMockOAuthProvider creates a new mock OAuth provider with default endpoints // 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 // MockSessionManager implements a mock session manager for testing
type MockSessionManager struct { type MockSessionManager struct {
Sessions map[string]*SessionData Sessions map[string]*SessionData
mu sync.RWMutex
// Configurable behaviors
GetSessionFunc func(r *http.Request) (*SessionData, error) GetSessionFunc func(r *http.Request) (*SessionData, error)
SaveSessionFunc func(r *http.Request, w http.ResponseWriter, session *SessionData) error SaveSessionFunc func(r *http.Request, w http.ResponseWriter, session *SessionData) error
DeleteSessionFunc func(r *http.Request, w http.ResponseWriter) error DeleteSessionFunc func(r *http.Request, w http.ResponseWriter) error
mu sync.RWMutex
// Simulation flags GetCallCount int32
SimulateError bool SaveCallCount int32
SimulateNotFound bool DeleteCallCount int32
SimulateError bool
// Tracking SimulateNotFound bool
GetCallCount int32
SaveCallCount int32
DeleteCallCount int32
} }
// NewMockSessionManager creates a new mock session manager // NewMockSessionManager creates a new mock session manager
@@ -370,23 +358,16 @@ func (m *MockSessionManager) Reset() {
// MockHTTPClient implements a mock HTTP client for testing // MockHTTPClient implements a mock HTTP client for testing
type MockHTTPClient struct { type MockHTTPClient struct {
// Response configuration ResponseFunc func(req *http.Request) (*http.Response, error)
ResponseFunc func(req *http.Request) (*http.Response, error)
// Default response settings
DefaultStatusCode int
DefaultBody string
DefaultHeaders map[string]string DefaultHeaders map[string]string
DefaultBody string
// Simulation flags Requests []*http.Request
SimulateTimeout bool RequestBodies [][]byte
SimulateError bool DefaultStatusCode int
TimeoutDuration time.Duration TimeoutDuration time.Duration
mu sync.Mutex
// Request tracking SimulateTimeout bool
Requests []*http.Request SimulateError bool
RequestBodies [][]byte
mu sync.Mutex
} }
// NewMockHTTPClient creates a new mock HTTP client // NewMockHTTPClient creates a new mock HTTP client
+25 -56
View File
@@ -15,38 +15,19 @@ import (
// It implements request coalescing, rate limiting, and circuit breaking // It implements request coalescing, rate limiting, and circuit breaking
// specifically for token refresh operations. // specifically for token refresh operations.
type RefreshCoordinator struct { type RefreshCoordinator struct {
// inFlightRefreshes tracks active refresh operations by refresh token hash inFlightRefreshes map[string]*refreshOperation
inFlightRefreshes map[string]*refreshOperation cleanupTimers map[string]*time.Timer
// refreshMutex protects the inFlightRefreshes map
refreshMutex sync.RWMutex
// sessionRefreshAttempts tracks refresh attempts per session
sessionRefreshAttempts map[string]*refreshAttemptTracker sessionRefreshAttempts map[string]*refreshAttemptTracker
// attemptsMutex protects sessionRefreshAttempts map delayedCleanupQueue chan delayedCleanupItem
attemptsMutex sync.RWMutex circuitBreaker *RefreshCircuitBreaker
metrics *RefreshMetrics
// Circuit breaker for refresh operations logger *Logger
circuitBreaker *RefreshCircuitBreaker stopChan chan struct{}
config RefreshCoordinatorConfig
// Configuration wg sync.WaitGroup
config RefreshCoordinatorConfig attemptsMutex sync.RWMutex
refreshMutex sync.RWMutex
// Metrics cleanupTimerMu sync.Mutex
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
} }
// RefreshCoordinatorConfig configures the refresh coordinator behavior // RefreshCoordinatorConfig configures the refresh coordinator behavior
@@ -89,18 +70,12 @@ func DefaultRefreshCoordinatorConfig() RefreshCoordinatorConfig {
// refreshOperation represents an in-flight refresh operation // refreshOperation represents an in-flight refresh operation
type refreshOperation struct { type refreshOperation struct {
// refreshToken being refreshed (for validation) startTime time.Time
result *refreshResult
done chan struct{}
refreshToken string refreshToken string
// result stores the final result mutex sync.RWMutex
result *refreshResult waiterCount int32
// 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
} }
// refreshResult contains the result of a refresh operation // refreshResult contains the result of a refresh operation
@@ -112,18 +87,12 @@ type refreshResult struct {
// refreshAttemptTracker tracks refresh attempts for a session // refreshAttemptTracker tracks refresh attempts for a session
type refreshAttemptTracker struct { type refreshAttemptTracker struct {
// attempts counts refresh attempts in current window lastAttemptTime time.Time
attempts int32 windowStartTime time.Time
// lastAttemptTime is the timestamp of the last attempt cooldownEndTime time.Time
lastAttemptTime time.Time attempts int32
// 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
consecutiveFailures int32 consecutiveFailures int32
inCooldown bool
} }
// RefreshMetrics tracks coordinator performance metrics // RefreshMetrics tracks coordinator performance metrics
@@ -140,18 +109,18 @@ type RefreshMetrics struct {
// delayedCleanupItem represents an item scheduled for delayed cleanup // delayedCleanupItem represents an item scheduled for delayed cleanup
type delayedCleanupItem struct { type delayedCleanupItem struct {
tokenHash string
cleanupAt time.Time cleanupAt time.Time
tokenHash string
} }
// RefreshCircuitBreaker implements a circuit breaker specifically for refresh operations // RefreshCircuitBreaker implements a circuit breaker specifically for refresh operations
type RefreshCircuitBreaker struct { type RefreshCircuitBreaker struct {
state int32 // 0=closed, 1=open, 2=half-open
failures int32
lastFailureTime time.Time lastFailureTime time.Time
lastSuccessTime time.Time lastSuccessTime time.Time
config RefreshCircuitBreakerConfig config RefreshCircuitBreakerConfig
mutex sync.RWMutex mutex sync.RWMutex
state int32
failures int32
} }
// RefreshCircuitBreakerConfig configures the refresh circuit breaker // RefreshCircuitBreakerConfig configures the refresh circuit breaker
+4 -4
View File
@@ -132,7 +132,7 @@ func testIssue53ReverseProxyHTTPS(t *testing.T) {
session.SetEmail("[email protected]") session.SetEmail("[email protected]")
// Azure may use opaque access tokens // Azure may use opaque access tokens
session.SetAccessToken("opaque-azure-access-token") 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") session.SetRefreshToken("azure-refresh-token")
// Save with proper security // Save with proper security
@@ -178,9 +178,9 @@ func testIssue53SameSiteCookies(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
proto string proto string
expectedSecure bool
expectedSameSite http.SameSite
description string description string
expectedSameSite http.SameSite
expectedSecure bool
}{ }{
{ {
name: "HTTPS via proxy", name: "HTTPS via proxy",
@@ -240,9 +240,9 @@ func testIssue60MissingClaimFields(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
description string
headers []traefikoidc.TemplatedHeader headers []traefikoidc.TemplatedHeader
shouldValidate bool shouldValidate bool
description string
}{ }{
{ {
name: "Direct claim access", name: "Direct claim access",
+555 -23
View File
@@ -8,6 +8,7 @@ import (
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -66,23 +67,160 @@ func generateSecureRandomString(length int) (string, error) {
// These are appended to the cookiePrefix to create full cookie names // These are appended to the cookiePrefix to create full cookie names
// #nosec G101 -- These are cookie name suffixes, not hardcoded credentials // #nosec G101 -- These are cookie name suffixes, not hardcoded credentials
const ( const (
mainCookieSuffix = "m" mainCookieSuffix = "m"
accessTokenSuffix = "a" accessTokenSuffix = "a"
refreshTokenSuffix = "r" refreshTokenSuffix = "r"
idTokenSuffix = "id" idTokenSuffix = "id"
defaultCookiePrefix = "_oidc_raczylo_" combinedCookieSuffix = "s" // Combined session cookie suffix
defaultCookiePrefix = "_oidc_raczylo_"
) )
const ( 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 absoluteSessionTimeout = 24 * time.Hour
minEncryptionKeyLength = 32 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. // compressToken compresses a JWT token using gzip compression if beneficial.
// It validates the token format, attempts compression, and verifies the compressed // It validates the token format, attempts compression, and verifies the compressed
// data can be decompressed correctly. Only compresses if it reduces size. // 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. // session object reuse and supports both HTTP and HTTPS schemes.
type SessionManager struct { type SessionManager struct {
sessionPool sync.Pool sessionPool sync.Pool
ctx context.Context
store sessions.Store store sessions.Store
logger *Logger logger *Logger
chunkManager *ChunkManager 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 memoryMonitor *TaskMemoryMonitor
cancel context.CancelFunc
cookieDomain string
cookiePrefix string
sessionMaxAge time.Duration
activeSessions int64 activeSessions int64
poolHits int64 poolHits int64
poolMisses int64 poolMisses int64
cleanupMutex sync.RWMutex
shutdownOnce sync.Once shutdownOnce sync.Once
forceHTTPS bool
cleanupDone bool
} }
// NewSessionManager creates a new SessionManager instance with secure defaults. // 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), accessTokenChunks: make(map[int]*sessions.Session),
refreshTokenChunks: make(map[int]*sessions.Session), refreshTokenChunks: make(map[int]*sessions.Session),
idTokenChunks: 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{}, refreshMutex: sync.Mutex{},
sessionMutex: sync.RWMutex{}, sessionMutex: sync.RWMutex{},
dirty: false, dirty: false,
@@ -349,6 +489,17 @@ func (sm *SessionManager) idTokenCookieName() string {
return sm.cookiePrefix + idTokenSuffix 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 // Shutdown gracefully shuts down the SessionManager and all its background tasks
func (sm *SessionManager) Shutdown() error { func (sm *SessionManager) Shutdown() error {
var shutdownErr error var shutdownErr error
@@ -650,7 +801,7 @@ func (sm *SessionManager) GetSessionMetrics() map[string]interface{} {
metrics["force_https"] = sm.forceHTTPS metrics["force_https"] = sm.forceHTTPS
metrics["absolute_timeout_hours"] = sm.sessionMaxAge.Hours() metrics["absolute_timeout_hours"] = sm.sessionMaxAge.Hours()
metrics["max_cookie_size"] = maxCookieSize 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 { if cookieStore, ok := sm.store.(*sessions.CookieStore); ok && len(cookieStore.Codecs) > 0 {
metrics["has_encryption"] = true 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. // GetSession retrieves or creates session data from the HTTP request.
// It loads the main session and all token chunk sessions, performing validation // It first tries to load from combined cookies (new format), falling back to legacy
// and timeout checks. The returned session must be explicitly returned to the pool // cookies if combined cookies don't exist. Performs validation and timeout checks.
// by calling returnToPoolSafely() to prevent memory leaks. // 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. // MEMORY LEAK FIX: Session is NOT returned to pool here - caller must call ReturnToPool() when done.
// Parameters: // Parameters:
// - r: The HTTP request containing session cookies. // - 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) 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 var err error
sessionData.mainSession, err = sm.store.Get(r, sm.mainCookieName()) sessionData.mainSession, err = sm.store.Get(r, sm.mainCookieName())
if err != nil { 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.refreshTokenCookieName(), sessionData.refreshTokenChunks)
sm.getTokenChunkSessions(r, sm.idTokenCookieName(), sessionData.idTokenChunks) 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 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. // getTokenChunkSessions loads all available token chunk sessions for a given token type.
// It iterates through numbered chunk sessions until no more are found, // It iterates through numbered chunk sessions until no more are found,
// populating the provided chunks map with the loaded sessions. // 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. // SessionData represents a user's authentication session with comprehensive token management.
// It handles main session data and supports large tokens that need to be // It handles main session data and supports large tokens that need to be
// split across multiple cookies due to browser size limitations. // split across multiple cookies due to browser size limitations.
// Supports both legacy (separate cookies) and combined (single compressed cookie) storage.
type SessionData struct { type SessionData struct {
manager *SessionManager manager *SessionManager
request *http.Request request *http.Request
// Legacy storage (kept for backward compatibility during migration)
mainSession *sessions.Session mainSession *sessions.Session
accessSession *sessions.Session accessSession *sessions.Session
@@ -939,6 +1198,12 @@ type SessionData struct {
idTokenChunks map[int]*sessions.Session 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 refreshMutex sync.Mutex
sessionMutex sync.RWMutex sessionMutex sync.RWMutex
@@ -966,6 +1231,7 @@ func (sd *SessionData) MarkDirty() {
// Save persists all session data including main session and token chunks. // Save persists all session data including main session and token chunks.
// It applies security options, saves all session components, and handles // It applies security options, saves all session components, and handles
// errors gracefully by continuing to save other components even if one fails. // errors gracefully by continuing to save other components even if one fails.
// Uses combined cookie storage for efficiency when useCombinedStorage is true.
// Parameters: // Parameters:
// - r: The HTTP request context for security option configuration. // - r: The HTTP request context for security option configuration.
// - w: The HTTP response writer for setting session cookies. // - 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.getSessionOptions(isSecure)
options = sd.manager.EnhanceSessionSecurity(options, r) 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.mainSession.Options = options
sd.accessSession.Options = options sd.accessSession.Options = options
sd.refreshSession.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.mainSession, "main")
saveOrLogError(sd.accessSession, "access token") saveOrLogError(sd.accessSession, "access token")
saveOrLogError(sd.refreshSession, "refresh token") saveOrLogError(sd.refreshSession, "refresh token")
saveOrLogError(sd.idTokenSession, "ID token") saveOrLogError(sd.idTokenSession, "ID token")
for i, sessionChunk := range sd.accessTokenChunks { for i, sessionChunk := range sd.accessTokenChunks {
@@ -1030,6 +1404,84 @@ func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error {
return firstErr 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. // clearSessionValues removes all values from a session and optionally expires it.
// This is used during session cleanup and logout operations. // This is used during session cleanup and logout operations.
// Parameters: // Parameters:
@@ -1263,6 +1715,12 @@ func (sd *SessionData) Reset() {
resetSession(sd.refreshSession) resetSession(sd.refreshSession)
resetSession(sd.idTokenSession) 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 // Clear redirect count to prevent leaking between sessions
if sd.mainSession != nil && sd.mainSession.Values != nil { if sd.mainSession != nil && sd.mainSession.Values != nil {
delete(sd.mainSession.Values, "redirect_count") delete(sd.mainSession.Values, "redirect_count")
@@ -1271,6 +1729,7 @@ func (sd *SessionData) Reset() {
sd.dirty = false sd.dirty = false
sd.inUse = false sd.inUse = false
sd.request = nil sd.request = nil
sd.useCombinedStorage = true // Reset to use combined storage by default
// Reset the refresh mutex to ensure clean state // Reset the refresh mutex to ensure clean state
// Note: We don't need to lock it since sessionMutex is already held // 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: // Returns:
// - true if the chunk is safe to store, false if it may exceed browser limits. // - true if the chunk is safe to store, false if it may exceed browser limits.
func validateChunkSize(chunkData string) bool { func validateChunkSize(chunkData string) bool {
// Estimate ~50% overhead for encoding, compare against ~2x maxCookieSize limit
estimatedEncodedSize := len(chunkData) + (len(chunkData) * 50 / 100) estimatedEncodedSize := len(chunkData) + (len(chunkData) * 50 / 100)
return estimatedEncodedSize <= maxBrowserCookieSize return estimatedEncodedSize <= maxCookieSize*2
} }
// isCorruptionMarker detects if data contains known corruption indicators. // isCorruptionMarker detects if data contains known corruption indicators.
@@ -2088,6 +2548,78 @@ func (sd *SessionData) getIDTokenUnsafe() string {
return result.Token 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. // SetIDToken stores an ID token with automatic compression and chunking.
// It validates the JWT format, compresses if beneficial, and splits into chunks // It validates the JWT format, compresses if beneficial, and splits into chunks
// if the token exceeds cookie size limits. Includes comprehensive validation. // if the token exceeds cookie size limits. Includes comprehensive validation.
+4 -5
View File
@@ -100,13 +100,12 @@ type Logger interface {
// and error handling to ensure data integrity and prevent security vulnerabilities // and error handling to ensure data integrity and prevent security vulnerabilities
// throughout the process. // throughout the process.
type ChunkManager struct { type ChunkManager struct {
logger Logger lastCleanup time.Time
mutex *sync.RWMutex logger Logger
// sessionMap provides bounded session storage to prevent memory leaks mutex *sync.RWMutex
sessionMap map[string]*SessionEntry sessionMap map[string]*SessionEntry
maxSessions int maxSessions int
sessionTTL time.Duration sessionTTL time.Duration
lastCleanup time.Time
} }
// NewChunkManager creates a new ChunkManager instance with proper initialization. // NewChunkManager creates a new ChunkManager instance with proper initialization.
@@ -361,8 +360,8 @@ func (cm *ChunkManager) StoreSession(key string, session *sessions.Session) {
if shouldEvict { if shouldEvict {
// Find oldest sessions to remove // Find oldest sessions to remove
type sessionAge struct { type sessionAge struct {
key string
lastUsed time.Time lastUsed time.Time
key string
} }
sessions := make([]sessionAge, 0, currentLocal) sessions := make([]sessionAge, 0, currentLocal)
+21 -21
View File
@@ -16,7 +16,7 @@ func TestTokenValidatorJWT(t *testing.T) {
validator := NewTokenValidator() validator := NewTokenValidator()
// Test valid JWT format (using base64url encoded parts that are long enough) // 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") err := validator.ValidateJWTFormat(validJWT, "test")
if err != nil { if err != nil {
t.Errorf("Expected valid JWT to pass, got error: %v", err) 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) { func TestSessionMapBounds_HardLimitEnforcement(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
description string
maxSessions int maxSessions int
sessionCount int sessionCount int
expectEviction bool expectEviction bool
description string
}{ }{
{ {
name: "within_limit", name: "within_limit",
@@ -760,18 +760,18 @@ func TestValidateJWTContent(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
token string token string
expectError bool
description string description string
expectError bool
}{ }{
{ {
name: "Valid JWT with required ID token claims", name: "Valid JWT with required ID token claims",
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImNsaWVudElkIiwiZXhwIjoxNjQ2MDY0MDAwLCJpYXQiOjE2NDYwNjA0MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImNsaWVudElkIiwiZXhwIjoxNjQ2MDY0MDAwLCJpYXQiOjE2NDYwNjA0MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
expectError: false, expectError: false,
description: "JWT with all required ID token claims should pass", description: "JWT with all required ID token claims should pass",
}, },
{ {
name: "JWT missing required claims", name: "JWT missing required claims",
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
expectError: true, expectError: true,
description: "JWT missing required claims should fail", description: "JWT missing required claims should fail",
}, },
@@ -810,8 +810,8 @@ func TestValidateJWTHeader(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
header string header string
expectError bool
description string description string
expectError bool
}{ }{
{ {
name: "Valid JWT header", name: "Valid JWT header",
@@ -865,9 +865,9 @@ func TestValidateJWTPayload(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
payload string payload string
description string
config TokenConfig config TokenConfig
expectError bool expectError bool
description string
}{ }{
{ {
name: "Valid ID token payload", name: "Valid ID token payload",
@@ -927,8 +927,8 @@ func TestValidateJWTSignature(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
signature string signature string
expectError bool
description string description string
expectError bool
}{ }{
{ {
name: "Valid signature", name: "Valid signature",
@@ -976,9 +976,9 @@ func TestValidateChunkStructure(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
description string
chunks []ChunkData chunks []ChunkData
expectError bool expectError bool
description string
}{ }{
{ {
name: "Valid chunk structure", name: "Valid chunk structure",
@@ -1055,11 +1055,11 @@ func TestValidateChunkData(t *testing.T) {
config := AccessTokenConfig config := AccessTokenConfig
tests := []struct { tests := []struct {
name string
chunk ChunkData chunk ChunkData
name string
description string
expectedTotal int expectedTotal int
expectError bool expectError bool
description string
}{ }{
{ {
name: "Valid chunk data", name: "Valid chunk data",
@@ -1218,13 +1218,13 @@ func TestGetToken(t *testing.T) {
cm := NewChunkManager(nil) cm := NewChunkManager(nil)
tests := []struct { tests := []struct {
name string
mainSession *sessions.Session mainSession *sessions.Session
chunks map[int]*sessions.Session chunks map[int]*sessions.Session
config TokenConfig name string
expectedToken string expectedToken string
expectError bool
description string description string
config TokenConfig
expectError bool
}{ }{
{ {
name: "Token from main session", name: "Token from main session",
@@ -1363,8 +1363,8 @@ func TestSerializeTokenToChunks(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
token string token string
expectError bool
description string description string
expectError bool
}{ }{
{ {
name: "Valid token serialization", name: "Valid token serialization",
@@ -1436,10 +1436,10 @@ func TestDeserializeTokenFromChunks(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
chunks []ChunkData
expectedToken string expectedToken string
expectError bool
description string description string
chunks []ChunkData
expectError bool
}{ }{
{ {
name: "Valid chunks deserialization", name: "Valid chunks deserialization",
@@ -1522,10 +1522,10 @@ func TestEncodeDecodeChunk(t *testing.T) {
cs := NewChunkSerializer(NewNoOpLogger()) cs := NewChunkSerializer(NewNoOpLogger())
tests := []struct { tests := []struct {
name string
chunk ChunkData chunk ChunkData
expectError bool name string
description string description string
expectError bool
}{ }{
{ {
name: "Valid chunk encoding/decoding", name: "Valid chunk encoding/decoding",
@@ -1619,10 +1619,10 @@ func TestValidateChunkIntegrity(t *testing.T) {
cs := NewChunkSerializer(NewNoOpLogger()) cs := NewChunkSerializer(NewNoOpLogger())
tests := []struct { tests := []struct {
name string
chunk ChunkData chunk ChunkData
expectError bool name string
description string description string
expectError bool
}{ }{
{ {
name: "Valid chunk integrity", name: "Valid chunk integrity",
+4 -4
View File
@@ -254,10 +254,10 @@ func (cs *ChunkSerializer) calculateChecksum(content string) string {
// ChunkData represents a single chunk of token data // ChunkData represents a single chunk of token data
type ChunkData struct { type ChunkData struct {
Index int // Position of this chunk in the sequence Content string
Total int // Total number of chunks for this token Checksum string
Content string // The actual chunk content Index int
Checksum string // Simple checksum for integrity verification Total int
} }
// EstimateChunkCount estimates how many chunks a token will need // EstimateChunkCount estimates how many chunks a token will need
+10 -11
View File
@@ -84,21 +84,19 @@ type TokenRetrievalResult struct {
// and error handling to ensure data integrity and prevent security vulnerabilities // and error handling to ensure data integrity and prevent security vulnerabilities
// throughout the process. // throughout the process.
type ChunkManager struct { type ChunkManager struct {
logger *Logger lastCleanup time.Time
mutex *sync.RWMutex ctx context.Context
ctx context.Context mutex *sync.RWMutex
cancel context.CancelFunc cancel context.CancelFunc
wg sync.WaitGroup // WaitGroup to track background goroutine completion
// sessionMap provides bounded session storage to prevent memory leaks
sessionMap map[string]*SessionEntry sessionMap map[string]*SessionEntry
logger *Logger
wg sync.WaitGroup
maxSessions int maxSessions int
sessionTTL time.Duration sessionTTL time.Duration
lastCleanup time.Time
cleanupRunning int32 // atomic flag to prevent concurrent cleanups
// Memory usage tracking
bytesAllocated int64 bytesAllocated int64
peakSessions int64 peakSessions int64
cleanupCount int64 cleanupCount int64
cleanupRunning int32
} }
// SessionEntry represents a session with expiration tracking // 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} 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)", err := fmt.Errorf("%s token chunk %d exceeds browser limit (%d bytes)",
config.Type, i, len(chunk)) config.Type, i, len(chunk))
return TokenRetrievalResult{Token: "", Error: err} return TokenRetrievalResult{Token: "", Error: err}
@@ -1199,8 +1198,8 @@ func (cm *ChunkManager) findOldestSessions(k int) []string {
// Collect all timestamps with keys // Collect all timestamps with keys
type sessionAge struct { type sessionAge struct {
key string
lastUsed time.Time lastUsed time.Time
key string
} }
sessions := make([]sessionAge, 0, len(cm.sessionMap)) sessions := make([]sessionAge, 0, len(cm.sessionMap))
+15 -15
View File
@@ -24,31 +24,31 @@ import (
// SessionTestCase represents a comprehensive session test scenario // SessionTestCase represents a comprehensive session test scenario
type SessionTestCase struct { type SessionTestCase struct {
name string
scenario string // "creation", "validation", "expiration", "persistence", "cleanup", "chunking", "security"
sessionType string // "user", "admin", "api", "guest", "csrf"
setup func(*SessionTestFramework) setup func(*SessionTestFramework)
execute func(*SessionTestFramework) error execute func(*SessionTestFramework) error
validate func(*testing.T, error, *SessionTestFramework) validate func(*testing.T, error, *SessionTestFramework)
cleanup func(*SessionTestFramework) cleanup func(*SessionTestFramework)
concurrent bool name string
scenario string
sessionType string
skipReason string
iterations int iterations int
timeout time.Duration timeout time.Duration
skipReason string concurrent bool
} }
// SessionTestFramework provides shared test infrastructure for session tests // SessionTestFramework provides shared test infrastructure for session tests
type SessionTestFramework struct { type SessionTestFramework struct {
t *testing.T t *testing.T
mockProvider *httptest.Server mockProvider *httptest.Server
testTokens map[string]string
metrics *SessionTestMetrics
config *SessionTestConfig
requests []*http.Request requests []*http.Request
responses []*httptest.ResponseRecorder responses []*httptest.ResponseRecorder
testTokens map[string]string
sessionIDs []string sessionIDs []string
mu sync.RWMutex
metrics *SessionTestMetrics
cleanupFuncs []func() cleanupFuncs []func()
config *SessionTestConfig mu sync.RWMutex
} }
// SessionTestMetrics tracks test performance metrics // SessionTestMetrics tracks test performance metrics
@@ -65,12 +65,12 @@ type SessionTestMetrics struct {
// SessionTestConfig holds test configuration // SessionTestConfig holds test configuration
type SessionTestConfig struct { type SessionTestConfig struct {
CookieDomain string
EncryptionKey string
MaxChunkSize int MaxChunkSize int
MaxSessions int MaxSessions int
EnableHTTPS bool
CookieDomain string
SessionTimeout time.Duration SessionTimeout time.Duration
EncryptionKey string EnableHTTPS bool
EnableCompression bool EnableCompression bool
} }
@@ -2849,9 +2849,9 @@ func TestSessionExpiryVsTokenExpiry(t *testing.T) {
scenarios := []struct { scenarios := []struct {
name string name string
expectedBehavior string
sessionAge time.Duration sessionAge time.Duration
tokenExpiry time.Duration tokenExpiry time.Duration
expectedBehavior string
sessionShouldExpire bool sessionShouldExpire bool
tokenShouldRefresh bool tokenShouldRefresh bool
}{ }{
@@ -2975,10 +2975,10 @@ func TestSessionCleanupOnTokenExpiry(t *testing.T) {
scenarios := []struct { scenarios := []struct {
name string name string
tokenExpiry time.Duration
shouldCleanup bool
shouldPreserve []string shouldPreserve []string
shouldRemove []string shouldRemove []string
tokenExpiry time.Duration
shouldCleanup bool
}{ }{
{ {
name: "Recently expired tokens - preserve session", name: "Recently expired tokens - preserve session",
+103 -334
View File
@@ -27,359 +27,128 @@ type TemplatedHeader struct {
// It provides all necessary settings to configure OpenID Connect authentication // It provides all necessary settings to configure OpenID Connect authentication
// with various providers like Auth0, Logto, or any standard OIDC provider. // with various providers like Auth0, Logto, or any standard OIDC provider.
type Config struct { 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"` DynamicClientRegistration *DynamicClientRegistrationConfig `json:"dynamicClientRegistration,omitempty"`
Redis *RedisConfig `json:"redis,omitempty"`
// AllowPrivateIPAddresses disables the security check that blocks private/internal IP addresses. HTTPClient *http.Client `json:"-"`
// By default, the plugin rejects URLs containing private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x) SecurityHeaders *SecurityHeadersConfig `json:"securityHeaders,omitempty"`
// to prevent SSRF attacks and ensure OIDC providers are publicly accessible. PostLogoutRedirectURI string `json:"postLogoutRedirectURI"`
// LogLevel string `json:"logLevel"`
// Enable this option ONLY when: LogoutURL string `json:"logoutURL"`
// - Your OIDC provider (e.g., Keycloak) runs on an internal network with private IPs ClientID string `json:"clientID"`
// - You have no DNS resolution available for internal services ClientSecret string `json:"clientSecret"`
// - Your entire stack runs in a Docker network or Kubernetes cluster with private addressing Audience string `json:"audience,omitempty"`
// CookiePrefix string `json:"cookiePrefix"`
// Security Warning: Enabling this option reduces SSRF protection. Only use in trusted CallbackURL string `json:"callbackURL"`
// network environments where the OIDC provider is known and controlled. SessionEncryptionKey string `json:"sessionEncryptionKey"`
// ProviderURL string `json:"providerURL"`
// Default: false (private IPs are blocked for security) RevocationURL string `json:"revocationURL"`
AllowPrivateIPAddresses bool `json:"allowPrivateIPAddresses,omitempty"` UserIdentifierClaim string `json:"userIdentifierClaim,omitempty"`
GroupClaimName string `json:"groupClaimName,omitempty"`
// MinimalHeaders reduces the number of headers forwarded to downstream services. RoleClaimName string `json:"roleClaimName,omitempty"`
// This helps prevent "431 Request Header Fields Too Large" errors when downstream CookieDomain string `json:"cookieDomain"`
// services have limited header buffer sizes. OIDCEndSessionURL string `json:"oidcEndSessionURL"`
// Scopes []string `json:"scopes"`
// When enabled (true): AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
// - Only forwards: X-Forwarded-User ExcludedURLs []string `json:"excludedURLs"`
// - Skips: X-Auth-Request-Token (full ID token), X-Auth-Request-Redirect AllowedUserDomains []string `json:"allowedUserDomains"`
// - Groups/roles headers (X-User-Groups, X-User-Roles) are still forwarded if configured AllowedUsers []string `json:"allowedUsers"`
// - Custom templated headers are still processed Headers []TemplatedHeader `json:"headers"`
// RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
// When disabled (false, default): SessionMaxAge int `json:"sessionMaxAge"`
// - Forwards all headers: X-Forwarded-User, X-Auth-Request-User, X-Auth-Request-Redirect, RateLimit int `json:"rateLimit"`
// X-Auth-Request-Token (full ID token) OverrideScopes bool `json:"overrideScopes"`
// DisableReplayDetection bool `json:"disableReplayDetection,omitempty"`
// Use this option when: RequireTokenIntrospection bool `json:"requireTokenIntrospection,omitempty"`
// - Downstream services return "431 Request Header Fields Too Large" errors AllowOpaqueTokens bool `json:"allowOpaqueTokens,omitempty"`
// - You don't need the full ID token forwarded to backend services StrictAudienceValidation bool `json:"strictAudienceValidation,omitempty"`
// - You want to reduce request overhead EnablePKCE bool `json:"enablePKCE"`
// ForceHTTPS bool `json:"forceHTTPS"`
// Default: false (all headers forwarded for backward compatibility) AllowPrivateIPAddresses bool `json:"allowPrivateIPAddresses,omitempty"`
MinimalHeaders bool `json:"minimalHeaders,omitempty"` MinimalHeaders bool `json:"minimalHeaders,omitempty"`
} }
// RedisConfig configures Redis cache backend settings for distributed caching. // RedisConfig configures Redis cache backend settings for distributed caching.
// All fields support both JSON and YAML configuration for compatibility with Traefik's // All fields support both JSON and YAML configuration for compatibility with Traefik's
// dynamic configuration (labels, YAML files, etc.) // dynamic configuration (labels, YAML files, etc.)
type RedisConfig struct { type RedisConfig struct {
// Enabled indicates if Redis caching should be used (default: false) KeyPrefix string `json:"keyPrefix" yaml:"keyPrefix"`
Enabled bool `json:"enabled" yaml:"enabled"` Address string `json:"address" yaml:"address"`
Password string `json:"password,omitempty" yaml:"password,omitempty"`
// Address is the Redis server address (e.g., "localhost:6379", "redis:6379") CacheMode string `json:"cacheMode" yaml:"cacheMode"`
Address string `json:"address" yaml:"address"` WriteTimeout int `json:"writeTimeout" yaml:"writeTimeout"`
CircuitBreakerThreshold int `json:"circuitBreakerThreshold" yaml:"circuitBreakerThreshold"`
// Password for Redis authentication (optional, leave empty for no auth) ConnectTimeout int `json:"connectTimeout" yaml:"connectTimeout"`
Password string `json:"password,omitempty" yaml:"password,omitempty"` ReadTimeout int `json:"readTimeout" yaml:"readTimeout"`
PoolSize int `json:"poolSize" yaml:"poolSize"`
// DB is the Redis database number to use (default: 0) HealthCheckInterval int `json:"healthCheckInterval" yaml:"healthCheckInterval"`
DB int `json:"db" yaml:"db"` CircuitBreakerTimeout int `json:"circuitBreakerTimeout" yaml:"circuitBreakerTimeout"`
DB int `json:"db" yaml:"db"`
// KeyPrefix is the prefix for all Redis keys (default: "traefikoidc:") HybridL1Size int `json:"hybridL1Size" yaml:"hybridL1Size"`
KeyPrefix string `json:"keyPrefix" yaml:"keyPrefix"` HybridL1MemoryMB int64 `json:"hybridL1MemoryMB" yaml:"hybridL1MemoryMB"`
Enabled bool `json:"enabled" yaml:"enabled"`
// PoolSize is the maximum number of socket connections (default: 10) EnableCircuitBreaker bool `json:"enableCircuitBreaker" yaml:"enableCircuitBreaker"`
PoolSize int `json:"poolSize" yaml:"poolSize"` TLSSkipVerify bool `json:"tlsSkipVerify" yaml:"tlsSkipVerify"`
EnableHealthCheck bool `json:"enableHealthCheck" yaml:"enableHealthCheck"`
// ConnectTimeout is the timeout for establishing connections in seconds (default: 5) EnableTLS bool `json:"enableTLS" yaml:"enableTLS"`
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"`
} }
// DynamicClientRegistrationConfig configures OIDC Dynamic Client Registration (RFC 7591) // DynamicClientRegistrationConfig configures OIDC Dynamic Client Registration (RFC 7591)
type DynamicClientRegistrationConfig struct { type DynamicClientRegistrationConfig struct {
// Enabled enables automatic client registration with the OIDC provider ClientMetadata *ClientRegistrationMetadata `json:"clientMetadata,omitempty"`
Enabled bool `json:"enabled"` InitialAccessToken string `json:"initialAccessToken,omitempty"`
RegistrationEndpoint string `json:"registrationEndpoint,omitempty"`
// InitialAccessToken is an optional bearer token for protected registration endpoints CredentialsFile string `json:"credentialsFile,omitempty"`
// Some providers require this token to authorize new client registrations Enabled bool `json:"enabled"`
InitialAccessToken string `json:"initialAccessToken,omitempty"` PersistCredentials bool `json:"persistCredentials"`
// 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"`
} }
// ClientRegistrationMetadata contains client metadata for dynamic registration (RFC 7591) // ClientRegistrationMetadata contains client metadata for dynamic registration (RFC 7591)
type ClientRegistrationMetadata struct { type ClientRegistrationMetadata struct {
// RedirectURIs is REQUIRED - array of redirect URIs for authorization TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
RedirectURIs []string `json:"redirect_uris"` TOSURI string `json:"tos_uri,omitempty"`
Scope string `json:"scope,omitempty"`
// ResponseTypes specifies OAuth 2.0 response types (default: ["code"]) ApplicationType string `json:"application_type,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"` SubjectType string `json:"subject_type,omitempty"`
ClientName string `json:"client_name,omitempty"`
// GrantTypes specifies OAuth 2.0 grant types (default: ["authorization_code"]) LogoURI string `json:"logo_uri,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"` ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
// ApplicationType is either "web" (default) or "native" JWKSURI string `json:"jwks_uri,omitempty"`
ApplicationType string `json:"application_type,omitempty"` ResponseTypes []string `json:"response_types,omitempty"`
Contacts []string `json:"contacts,omitempty"`
// Contacts is an array of email addresses for responsible parties RedirectURIs []string `json:"redirect_uris"`
Contacts []string `json:"contacts,omitempty"` DefaultACRValues []string `json:"default_acr_values,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
// ClientName is a human-readable name for the client DefaultMaxAge int `json:"default_max_age,omitempty"`
ClientName string `json:"client_name,omitempty"` RequireAuthTime bool `json:"require_auth_time,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"`
} }
// SecurityHeadersConfig configures security headers for the plugin // SecurityHeadersConfig configures security headers for the plugin
type SecurityHeadersConfig struct { type SecurityHeadersConfig struct {
// Enable security headers (default: true) CustomHeaders map[string]string `json:"customHeaders,omitempty"`
Enabled bool `json:"enabled"` PermissionsPolicy string `json:"permissionsPolicy,omitempty"`
Profile string `json:"profile"`
// Security profile: "default", "strict", "development", "api", or "custom" ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty"`
Profile string `json:"profile"` CrossOriginResourcePolicy string `json:"crossOriginResourcePolicy,omitempty"`
CrossOriginOpenerPolicy string `json:"crossOriginOpenerPolicy,omitempty"`
// Content Security Policy CrossOriginEmbedderPolicy string `json:"crossOriginEmbedderPolicy,omitempty"`
ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty"` FrameOptions string `json:"frameOptions,omitempty"`
ContentTypeOptions string `json:"contentTypeOptions,omitempty"`
// HSTS settings XSSProtection string `json:"xssProtection,omitempty"`
StrictTransportSecurity bool `json:"strictTransportSecurity"` ReferrerPolicy string `json:"referrerPolicy,omitempty"`
StrictTransportSecurityMaxAge int `json:"strictTransportSecurityMaxAge"` // seconds CORSAllowedHeaders []string `json:"corsAllowedHeaders,omitempty"`
StrictTransportSecuritySubdomains bool `json:"strictTransportSecuritySubdomains"` CORSAllowedOrigins []string `json:"corsAllowedOrigins,omitempty"`
StrictTransportSecurityPreload bool `json:"strictTransportSecurityPreload"` CORSAllowedMethods []string `json:"corsAllowedMethods,omitempty"`
StrictTransportSecurityMaxAge int `json:"strictTransportSecurityMaxAge"`
// Frame options: "DENY", "SAMEORIGIN", or "ALLOW-FROM uri" CORSMaxAge int `json:"corsMaxAge"`
FrameOptions string `json:"frameOptions,omitempty"` StrictTransportSecurityPreload bool `json:"strictTransportSecurityPreload"`
StrictTransportSecuritySubdomains bool `json:"strictTransportSecuritySubdomains"`
// Content type options (default: "nosniff") CORSEnabled bool `json:"corsEnabled"`
ContentTypeOptions string `json:"contentTypeOptions,omitempty"` Enabled bool `json:"enabled"`
CORSAllowCredentials bool `json:"corsAllowCredentials"`
// XSS protection (default: "1; mode=block") StrictTransportSecurity bool `json:"strictTransportSecurity"`
XSSProtection string `json:"xssProtection,omitempty"` DisableServerHeader bool `json:"disableServerHeader"`
DisablePoweredByHeader bool `json:"disablePoweredByHeader"`
// 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"`
} }
const ( const (
+1 -1
View File
@@ -17,8 +17,8 @@ type ShardedCache struct {
// cacheShard represents a single shard with its own mutex and data map. // cacheShard represents a single shard with its own mutex and data map.
type cacheShard struct { type cacheShard struct {
mu sync.RWMutex
items map[string]*shardedCacheItem items map[string]*shardedCacheItem
mu sync.RWMutex
} }
// shardedCacheItem represents an item in the sharded cache with expiration. // shardedCacheItem represents an item in the sharded cache with expiration.
+18 -33
View File
@@ -18,33 +18,20 @@ var (
// ResourceManager manages shared resources across all middleware instances // ResourceManager manages shared resources across all middleware instances
// to prevent duplication and goroutine leaks when Traefik recreates middleware // to prevent duplication and goroutine leaks when Traefik recreates middleware
type ResourceManager struct { 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 references map[string]*int32
referencesMu sync.RWMutex caches map[string]interface{}
httpClients map[string]*http.Client
// Logger tasks map[string]*BackgroundTask
logger *Logger
// Shutdown coordination
shutdownOnce sync.Once
shutdownChan chan struct{} shutdownChan chan struct{}
pools map[string]*GoroutinePool
logger *Logger
wg sync.WaitGroup 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 // 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 // GoroutinePool provides a pool of workers for controlled concurrency
type GoroutinePool struct { type GoroutinePool struct {
maxWorkers int
taskQueue chan func() taskQueue chan func()
workerWG sync.WaitGroup
shutdownOnce sync.Once
shutdownChan chan struct{} shutdownChan chan struct{}
logger *Logger logger *Logger
started int32
// Condition variable for efficient Wait() without busy-polling
taskCond *sync.Cond 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 // 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 // GenericCache provides a simple cache implementation for testing
type GenericCache struct { type GenericCache struct {
data map[string]interface{} data map[string]interface{}
mu sync.RWMutex
ttl time.Duration
logger *Logger logger *Logger
stopChan chan struct{} stopChan chan struct{}
ttl time.Duration
mu sync.RWMutex
} }
// NewGenericCache creates a new generic cache // NewGenericCache creates a new generic cache
+13 -20
View File
@@ -9,26 +9,19 @@ import (
// TestConfig manages test execution configuration and performance settings // TestConfig manages test execution configuration and performance settings
type TestConfig struct { type TestConfig struct {
// Test execution modes MemoryThreshold float64
ExtendedTests bool // Run extended/stress tests MaxConcurrency int
LongTests bool // Run long-running performance tests MaxIterations int
QuickMode bool // Quick smoke tests only DefaultTimeout time.Duration
GoroutineGrowth int
// Performance settings CacheSize int
MaxConcurrency int // Maximum concurrent operations CleanupInterval time.Duration
MaxIterations int // Maximum test iterations LongTests bool
DefaultTimeout time.Duration // Default test timeout QuickMode bool
MemoryThreshold float64 // Memory growth threshold in MB ExtendedTests bool
GoroutineGrowth int // Acceptable goroutine growth MemoryStressTest bool
ConcurrencyTest bool
// Cache settings for tests LeakDetection bool
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
} }
// NewTestConfig creates a test configuration based on flags and environment // NewTestConfig creates a test configuration based on flags and environment
+4 -4
View File
@@ -21,11 +21,11 @@ type TestFramework struct {
server *httptest.Server server *httptest.Server
oidc *TraefikOidc oidc *TraefikOidc
config *Config config *Config
cleanup []func()
mocks *TestMocks mocks *TestMocks
fixtures *TestFixtures fixtures *TestFixtures
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey publicKey *rsa.PublicKey
cleanup []func()
mu sync.Mutex mu sync.Mutex
} }
@@ -457,12 +457,12 @@ func GetTestFramework() *TestFramework {
// TestScenario represents a test scenario // TestScenario represents a test scenario
type TestScenario struct { type TestScenario struct {
Name string
Setup func(*TestFramework) Setup func(*TestFramework)
Request func(*TestFramework) *http.Request Request func(*TestFramework) *http.Request
ExpectedStatus int
ExpectedBody string
Validate func(*TestFramework, *httptest.ResponseRecorder) Validate func(*TestFramework, *httptest.ResponseRecorder)
Name string
ExpectedBody string
ExpectedStatus int
} }
// RunScenarios executes a set of test scenarios // RunScenarios executes a set of test scenarios
+15 -15
View File
@@ -17,10 +17,10 @@ import (
// GlobalTestCleanup tracks and cleans up test resources // GlobalTestCleanup tracks and cleans up test resources
type GlobalTestCleanup struct { type GlobalTestCleanup struct {
mu sync.Mutex
servers []*httptest.Server servers []*httptest.Server
tasks []*BackgroundTask tasks []*BackgroundTask
caches []interface{ Close() } caches []interface{ Close() }
mu sync.Mutex
} }
var globalCleanup = &GlobalTestCleanup{} var globalCleanup = &GlobalTestCleanup{}
@@ -187,13 +187,13 @@ func GetTestDuration(normal time.Duration) time.Duration {
// UnifiedMockSession provides a comprehensive mock for the Session interface // UnifiedMockSession provides a comprehensive mock for the Session interface
type UnifiedMockSession struct { type UnifiedMockSession struct {
mu sync.RWMutex
data map[string]interface{} data map[string]interface{}
callCounts map[string]int64 callCounts map[string]int64
errors map[string]error errors map[string]error
delays map[string]time.Duration delays map[string]time.Duration
destroyed bool
destroyCount int64 destroyCount int64
mu sync.RWMutex
destroyed bool
} }
// NewUnifiedMockSession creates a new mock session with default behavior // 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 // UnifiedMockTokenVerifier provides a comprehensive mock for token verification
type UnifiedMockTokenVerifier struct { type UnifiedMockTokenVerifier struct {
mu sync.RWMutex
validTokens map[string]bool validTokens map[string]bool
tokenMetadata map[string]map[string]interface{} tokenMetadata map[string]map[string]interface{}
callCounts map[string]int64 callCounts map[string]int64
errors map[string]error errors map[string]error
delays map[string]time.Duration delays map[string]time.Duration
verificationFunc func(string) error verificationFunc func(string) error
mu sync.RWMutex
} }
// NewUnifiedMockTokenVerifier creates a new mock token verifier // 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 // UnifiedMockTokenCache provides a comprehensive mock for token caching
type UnifiedMockTokenCache struct { type UnifiedMockTokenCache struct {
mu sync.RWMutex
cache map[string]TestCacheEntry cache map[string]TestCacheEntry
callCounts map[string]int64 callCounts map[string]int64
errors map[string]error errors map[string]error
delays map[string]time.Duration delays map[string]time.Duration
hitRate float64 hitRate float64
mu sync.RWMutex
} }
// TestCacheEntry represents a cached token entry for testing // TestCacheEntry represents a cached token entry for testing
type TestCacheEntry struct { type TestCacheEntry struct {
Token string
ExpiresAt time.Time ExpiresAt time.Time
Metadata map[string]interface{} Metadata map[string]interface{}
Token string
} }
// NewUnifiedMockTokenCache creates a new mock token cache // NewUnifiedMockTokenCache creates a new mock token cache
@@ -539,39 +539,39 @@ func (m *UnifiedMockTokenCache) Clear() {
// TableTestCase represents a standardized test case structure // TableTestCase represents a standardized test case structure
type TableTestCase struct { type TableTestCase struct {
Name string
Description string
Input interface{} Input interface{}
Expected interface{} Expected interface{}
ExpectedError error ExpectedError error
Setup func(*testing.T) error Setup func(*testing.T) error
Teardown func(*testing.T) error Teardown func(*testing.T) error
Timeout time.Duration Name string
Description string
SkipReason string SkipReason string
Tags []string Tags []string
Timeout time.Duration
Parallel bool Parallel bool
} }
// MemoryLeakTestCase represents a test case specifically for memory leak detection // MemoryLeakTestCase represents a test case specifically for memory leak detection
type MemoryLeakTestCase struct { type MemoryLeakTestCase struct {
Operation func() error
Setup func() error
Teardown func() error
Name string Name string
Description string Description string
Operation func() error
Iterations int Iterations int
MaxGoroutineGrowth int MaxGoroutineGrowth int
MaxMemoryGrowthMB float64 MaxMemoryGrowthMB float64
Setup func() error
Teardown func() error
GCBetweenRuns bool
Timeout time.Duration Timeout time.Duration
GCBetweenRuns bool
} }
// TestSuiteRunner provides utilities for running table-driven tests // TestSuiteRunner provides utilities for running table-driven tests
type TestSuiteRunner struct { type TestSuiteRunner struct {
parallelTests bool
timeout time.Duration
beforeEach func(*testing.T) beforeEach func(*testing.T)
afterEach func(*testing.T) afterEach func(*testing.T)
timeout time.Duration
parallelTests bool
} }
// NewTestSuiteRunner creates a new test suite runner // NewTestSuiteRunner creates a new test suite runner
+1 -1
View File
@@ -20,9 +20,9 @@ func generateRandomString(length int) string {
// Test createCaseInsensitiveStringMap function // Test createCaseInsensitiveStringMap function
func TestCreateCaseInsensitiveStringMap(t *testing.T) { func TestCreateCaseInsensitiveStringMap(t *testing.T) {
tests := []struct { tests := []struct {
expected map[string]struct{}
name string name string
items []string items []string
expected map[string]struct{}
}{ }{
{ {
name: "Mixed case items", name: "Mixed case items",
+12 -12
View File
@@ -16,18 +16,18 @@ import (
// IntrospectionResponse represents the response from an OAuth 2.0 token introspection endpoint. // 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. // Per RFC 7662, this contains information about the token's validity and properties.
type IntrospectionResponse struct { type IntrospectionResponse struct {
Active bool `json:"active"` // REQUIRED - whether the token is currently active Scope string `json:"scope,omitempty"`
Scope string `json:"scope,omitempty"` // Space-separated list of scopes ClientID string `json:"client_id,omitempty"`
ClientID string `json:"client_id,omitempty"` // Client identifier for the token Username string `json:"username,omitempty"`
Username string `json:"username,omitempty"` // Human-readable identifier for the resource owner TokenType string `json:"token_type,omitempty"`
TokenType string `json:"token_type,omitempty"` // Type of token (e.g., "Bearer") Sub string `json:"sub,omitempty"`
Exp int64 `json:"exp,omitempty"` // Expiration time (seconds since epoch) Aud string `json:"aud,omitempty"`
Iat int64 `json:"iat,omitempty"` // Issued at time (seconds since epoch) Iss string `json:"iss,omitempty"`
Nbf int64 `json:"nbf,omitempty"` // Not before time (seconds since epoch) Jti string `json:"jti,omitempty"`
Sub string `json:"sub,omitempty"` // Subject of the token Exp int64 `json:"exp,omitempty"`
Aud string `json:"aud,omitempty"` // Intended audience Iat int64 `json:"iat,omitempty"`
Iss string `json:"iss,omitempty"` // Issuer Nbf int64 `json:"nbf,omitempty"`
Jti string `json:"jti,omitempty"` // JWT ID Active bool `json:"active"`
} }
// introspectToken performs OAuth 2.0 Token Introspection (RFC 7662) for an opaque token. // introspectToken performs OAuth 2.0 Token Introspection (RFC 7662) for an opaque token.
+10 -26
View File
@@ -8,37 +8,21 @@ import (
// TokenResilienceConfig centralizes resilience configuration for token operations // TokenResilienceConfig centralizes resilience configuration for token operations
type TokenResilienceConfig struct { type TokenResilienceConfig struct {
// Circuit breaker configuration for token operations MetadataCacheConfig MetadataCacheResilienceConfig
CircuitBreakerEnabled bool RetryConfig RetryConfig
CircuitBreakerConfig CircuitBreakerConfig CircuitBreakerConfig CircuitBreakerConfig
CircuitBreakerEnabled bool
// Retry configuration for token operations RetryEnabled bool
RetryEnabled bool
RetryConfig RetryConfig
// Metadata cache progressive grace period configuration
MetadataCacheConfig MetadataCacheResilienceConfig
} }
// MetadataCacheResilienceConfig defines resilience settings for metadata cache // MetadataCacheResilienceConfig defines resilience settings for metadata cache
type MetadataCacheResilienceConfig struct { type MetadataCacheResilienceConfig struct {
// EnableProgressiveGracePeriod allows extending cache TTL on failures SecurityCriticalFields []string
EnableProgressiveGracePeriod bool InitialGracePeriod time.Duration
ExtendedGracePeriod time.Duration
// InitialGracePeriod is the first extension when service is unavailable (5 minutes) MaxGracePeriod time.Duration
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
SecurityCriticalMaxGracePeriod time.Duration SecurityCriticalMaxGracePeriod time.Duration
EnableProgressiveGracePeriod bool
// SecurityCriticalFields defines which metadata fields are security-critical
SecurityCriticalFields []string
} }
// DefaultTokenResilienceConfig returns the default resilience configuration for token operations // DefaultTokenResilienceConfig returns the default resilience configuration for token operations
@@ -90,11 +74,11 @@ func DefaultMetadataCacheResilienceConfig() MetadataCacheResilienceConfig {
// TokenResilienceManager coordinates resilience mechanisms for token operations // TokenResilienceManager coordinates resilience mechanisms for token operations
type TokenResilienceManager struct { type TokenResilienceManager struct {
config TokenResilienceConfig
errorRecoveryManager *ErrorRecoveryManager errorRecoveryManager *ErrorRecoveryManager
circuitBreaker *CircuitBreaker circuitBreaker *CircuitBreaker
retryExecutor *RetryExecutor retryExecutor *RetryExecutor
logger *Logger logger *Logger
config TokenResilienceConfig
} }
// NewTokenResilienceManager creates a new token resilience manager // NewTokenResilienceManager creates a new token resilience manager
+25 -25
View File
@@ -26,10 +26,10 @@ import (
// Test tokens used across multiple test files // Test tokens used across multiple test files
var ( var (
ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" // trufflehog:ignore
ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" // trufflehog:ignore
ValidRefreshToken = "refresh_token_abc123" ValidRefreshToken = "refresh_token_abc123"
MinimalValidJWT = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0." MinimalValidJWT = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0." // trufflehog:ignore
InvalidTokenOneDot = "invalid.token" InvalidTokenOneDot = "invalid.token"
InvalidTokenNoDots = "invalidtoken" InvalidTokenNoDots = "invalidtoken"
InvalidTokenThreeDots = "invalid..token" InvalidTokenThreeDots = "invalid..token"
@@ -43,8 +43,8 @@ type TestTokens struct {
func NewTestTokens() *TestTokens { func NewTestTokens() *TestTokens {
return &TestTokens{ return &TestTokens{
validJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", validJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", // trufflehog:ignore
expiredJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjoxMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", expiredJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjoxMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", // trufflehog:ignore
} }
} }
@@ -137,12 +137,12 @@ func TestOpaqueTokenDetection(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
token string token string
isOpaque bool
description string description string
isOpaque bool
}{ }{
{ {
name: "JWT token with 3 parts", name: "JWT token with 3 parts",
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
isOpaque: false, isOpaque: false,
description: "Standard JWT with header.payload.signature", description: "Standard JWT with header.payload.signature",
}, },
@@ -218,7 +218,7 @@ func TestOpaqueTokenValidation(t *testing.T) {
}, },
{ {
name: "Valid JWT token", name: "Valid JWT token",
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
wantError: false, wantError: false,
}, },
} }
@@ -253,8 +253,8 @@ func TestOpaqueTokenStorage(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
token string token string
shouldStore bool
description string description string
shouldStore bool
}{ }{
{ {
name: "Valid opaque token", name: "Valid opaque token",
@@ -264,7 +264,7 @@ func TestOpaqueTokenStorage(t *testing.T) {
}, },
{ {
name: "Valid JWT token", name: "Valid JWT token",
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
shouldStore: true, shouldStore: true,
description: "Standard JWT with three parts", description: "Standard JWT with three parts",
}, },
@@ -754,11 +754,11 @@ func TestDetectTokenType(t *testing.T) {
} }
testCases := []struct { testCases := []struct {
name string
jwt *JWT jwt *JWT
name string
token string token string
expectedID bool
description string description string
expectedID bool
}{ }{
{ {
name: "ID token with nonce", name: "ID token with nonce",
@@ -1233,16 +1233,16 @@ func TestExtractTime(t *testing.T) {
validator := NewTokenValidator(nil) validator := NewTokenValidator(nil)
tests := []struct { tests := []struct {
name string
claim interface{} claim interface{}
name string
expected bool expected bool
}{ }{
{"float64", float64(1609459200), true}, {name: "float64", claim: float64(1609459200), expected: true},
{"int64", int64(1609459200), true}, {name: "int64", claim: int64(1609459200), expected: true},
{"int", int(1609459200), true}, {name: "int", claim: int(1609459200), expected: true},
{"string", "not a timestamp", false}, {name: "string", claim: "not a timestamp", expected: false},
{"nil", nil, false}, {name: "nil", claim: nil, expected: false},
{"map", map[string]interface{}{}, false}, {name: "map", claim: map[string]interface{}{}, expected: false},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -1556,11 +1556,11 @@ func TestTokenCorruption(t *testing.T) {
validJWT := testTokens.CreateLargeValidJWT(100) validJWT := testTokens.CreateLargeValidJWT(100)
tests := []struct { tests := []struct {
corruptionScenario func(*SessionData)
name string name string
tokenSize int tokenSize int
iterations int iterations int
expectConsistent bool expectConsistent bool
corruptionScenario func(*SessionData)
}{ }{
{ {
name: "Small token - multiple retrievals", name: "Small token - multiple retrievals",
@@ -1803,14 +1803,14 @@ func TestTokenValidation(t *testing.T) {
t.Run("TokenExpiryValidation", func(t *testing.T) { t.Run("TokenExpiryValidation", func(t *testing.T) {
now := time.Now() now := time.Now()
tests := []struct { tests := []struct {
name string
exp time.Time exp time.Time
name string
expectValid bool expectValid bool
}{ }{
{"Future expiry", now.Add(time.Hour), true}, {name: "Future expiry", exp: now.Add(time.Hour), expectValid: true},
{"Just expired", now.Add(-time.Second), false}, {name: "Just expired", exp: now.Add(-time.Second), expectValid: false},
{"Long expired", now.Add(-24 * time.Hour), false}, {name: "Long expired", exp: now.Add(-24 * time.Hour), expectValid: false},
{"Far future", now.Add(365 * 24 * time.Hour), true}, {name: "Far future", exp: now.Add(365 * 24 * time.Hour), expectValid: true},
} }
for _, tt := range tests { for _, tt := range tests {
+3 -3
View File
@@ -27,12 +27,12 @@ func NewTokenValidator(logger *Logger) *TokenValidator {
// TokenValidationResult contains the result of token validation // TokenValidationResult contains the result of token validation
type TokenValidationResult struct { type TokenValidationResult struct {
Valid bool Error error
TokenType string
Claims map[string]interface{} Claims map[string]interface{}
Expiry *time.Time Expiry *time.Time
IssuedAt *time.Time IssuedAt *time.Time
Error error TokenType string
Valid bool
} }
// ValidateToken performs comprehensive token validation // ValidateToken performs comprehensive token validation
+33 -35
View File
@@ -55,9 +55,9 @@ type ProviderMetadata struct {
JWKSURL string `json:"jwks_uri"` JWKSURL string `json:"jwks_uri"`
RevokeURL string `json:"revocation_endpoint"` RevokeURL string `json:"revocation_endpoint"`
EndSessionURL string `json:"end_session_endpoint"` EndSessionURL string `json:"end_session_endpoint"`
IntrospectionURL string `json:"introspection_endpoint,omitempty"` // OAuth 2.0 Token Introspection (RFC 7662) IntrospectionURL string `json:"introspection_endpoint,omitempty"`
ScopesSupported []string `json:"scopes_supported,omitempty"` // Supported scopes from discovery RegistrationURL string `json:"registration_endpoint,omitempty"`
RegistrationURL string `json:"registration_endpoint,omitempty"` // OIDC Dynamic Client Registration (RFC 7591) ScopesSupported []string `json:"scopes_supported,omitempty"`
} }
// TraefikOidc is the main middleware struct that implements OIDC authentication for Traefik. // 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 // the complete authentication flow. It's designed to work seamlessly with Traefik's
// plugin system and provides flexible configuration options. // plugin system and provides flexible configuration options.
type TraefikOidc struct { type TraefikOidc struct {
lastMetadataRetryTime time.Time
jwkCache JWKCacheInterface jwkCache JWKCacheInterface
jwtVerifier JWTVerifier jwtVerifier JWTVerifier
ctx context.Context ctx context.Context
tokenVerifier TokenVerifier tokenVerifier TokenVerifier
next http.Handler next http.Handler
tokenExchanger TokenExchanger tokenExchanger TokenExchanger
tokenBlacklist CacheInterface
tokenTypeCache CacheInterface
introspectionCache CacheInterface
initComplete chan struct{} initComplete chan struct{}
limiter *rate.Limiter limiter *rate.Limiter
tokenBlacklist CacheInterface
tokenTypeCache CacheInterface // Cache for token type detection results
headerTemplates map[string]*template.Template headerTemplates map[string]*template.Template
sessionManager *SessionManager sessionManager *SessionManager
tokenCleanupStopChan chan struct{} tokenCleanupStopChan chan struct{}
@@ -94,50 +96,46 @@ type TraefikOidc struct {
errorRecoveryManager *ErrorRecoveryManager errorRecoveryManager *ErrorRecoveryManager
tokenResilienceManager *TokenResilienceManager tokenResilienceManager *TokenResilienceManager
goroutineWG *sync.WaitGroup goroutineWG *sync.WaitGroup
clientSecret string dcrConfig *DynamicClientRegistrationConfig
clientID string dynamicClientRegistrar *DynamicClientRegistrar
audience string // Expected JWT audience, defaults to clientID scopeFilter *ScopeFilter
roleClaimName string // JWT claim name for extracting roles, defaults to "roles" securityHeadersApplier func(http.ResponseWriter, *http.Request)
groupClaimName string // JWT claim name for extracting groups, defaults to "groups" userIdentifierClaim string
userIdentifierClaim string // JWT claim for user identification, defaults to "email" revocationURL string
name string name string
redirURLPath string redirURLPath string
logoutURLPath string logoutURLPath string
metadataMu sync.RWMutex // Protects metadata endpoint fields
tokenURL string tokenURL string
authURL string authURL string
endSessionURL string endSessionURL string
postLogoutRedirectURI string postLogoutRedirectURI string
jwksURL string jwksURL string
issuerURL string issuerURL string
revocationURL string groupClaimName string
introspectionURL string // OAuth 2.0 Token Introspection endpoint (RFC 7662) introspectionURL string
providerURL string providerURL string
roleClaimName string
audience string
clientID string
clientSecret string
registrationURL string
scopesSupported []string
scopes []string scopes []string
refreshGracePeriod time.Duration refreshGracePeriod time.Duration
introspectionCache CacheInterface // Cache for token introspection results metadataMu sync.RWMutex
shutdownOnce sync.Once shutdownOnce sync.Once
metadataRetryMutex sync.Mutex
firstRequestMutex sync.Mutex firstRequestMutex sync.Mutex
forceHTTPS bool minimalHeaders 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
firstRequestReceived bool firstRequestReceived bool
requireTokenIntrospection bool
metadataRefreshStarted bool metadataRefreshStarted bool
lastMetadataRetryTime time.Time // Track last metadata retry for failed state recovery allowPrivateIPAddresses bool
metadataRetryMutex sync.Mutex // Protects lastMetadataRetryTime disableReplayDetection bool
allowPrivateIPAddresses bool // Allow private IP addresses in URLs (for internal networks) allowOpaqueTokens bool
minimalHeaders bool // Reduce headers to prevent 431 errors strictAudienceValidation bool
securityHeadersApplier func(http.ResponseWriter, *http.Request) overrideScopes bool
scopeFilter *ScopeFilter // NEW - for discovery-based scope filtering enablePKCE bool
scopesSupported []string // NEW - from provider metadata forceHTTPS bool
suppressDiagnosticLogs bool
// Dynamic Client Registration (RFC 7591)
dynamicClientRegistrar *DynamicClientRegistrar
dcrConfig *DynamicClientRegistrationConfig
registrationURL string // OIDC Dynamic Client Registration endpoint
} }
+30 -48
View File
@@ -25,27 +25,21 @@ const (
// UniversalCacheConfig provides configuration for the universal cache // UniversalCacheConfig provides configuration for the universal cache
type UniversalCacheConfig struct { type UniversalCacheConfig struct {
Strategy CacheStrategy
Logger *Logger
JWKConfig *JWKCacheConfig
MetadataConfig *MetadataCacheConfig
TokenConfig *TokenCacheConfig
Type CacheType Type CacheType
MaxSize int
MaxMemoryBytes int64
DefaultTTL time.Duration DefaultTTL time.Duration
CleanupInterval time.Duration CleanupInterval time.Duration
EnableCompression bool MaxMemoryBytes int64
MaxSize int
EnableAutoCleanup bool
EnableMemoryLimit bool
EnableMetrics bool EnableMetrics bool
EnableAutoCleanup bool // For backward compatibility EnableCompression bool
EnableMemoryLimit bool // For backward compatibility SkipAutoCleanup bool
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
} }
// TokenCacheConfig provides token-specific cache configuration // TokenCacheConfig provides token-specific cache configuration
@@ -57,11 +51,11 @@ type TokenCacheConfig struct {
// MetadataCacheConfig provides metadata-specific cache configuration // MetadataCacheConfig provides metadata-specific cache configuration
type MetadataCacheConfig struct { type MetadataCacheConfig struct {
SecurityCriticalFields []string
GracePeriod time.Duration GracePeriod time.Duration
ExtendedGracePeriod time.Duration ExtendedGracePeriod time.Duration
MaxGracePeriod time.Duration MaxGracePeriod time.Duration
SecurityCriticalMaxGracePeriod time.Duration SecurityCriticalMaxGracePeriod time.Duration
SecurityCriticalFields []string
} }
// JWKCacheConfig provides JWK-specific cache configuration // JWKCacheConfig provides JWK-specific cache configuration
@@ -73,48 +67,36 @@ type JWKCacheConfig struct {
// CacheItem represents a single cache entry // CacheItem represents a single cache entry
type CacheItem struct { type CacheItem struct {
Key string
Value interface{}
Size int64
ExpiresAt time.Time ExpiresAt time.Time
LastAccessed time.Time LastAccessed time.Time
AccessCount int64 Value interface{}
Metadata map[string]interface{}
element *list.Element
Key string
CacheType CacheType CacheType CacheType
Size int64
// Type-specific metadata AccessCount int64
Metadata map[string]interface{}
// LRU list element reference
element *list.Element
} }
// UniversalCache provides a single, unified cache implementation // UniversalCache provides a single, unified cache implementation
// that replaces all other cache types // that replaces all other cache types
type UniversalCache struct { type UniversalCache struct {
mu sync.RWMutex config UniversalCacheConfig
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
ctx context.Context ctx context.Context
backend backends.CacheBackend
logger *Logger
lruList *list.List
items map[string]*CacheItem
cancel context.CancelFunc cancel context.CancelFunc
cleanupTicker *time.Ticker cleanupTicker *time.Ticker
wg sync.WaitGroup 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 // NewUniversalCache creates a new universal cache instance
+11 -13
View File
@@ -13,22 +13,20 @@ import (
// It runs a single consolidated cleanup goroutine for all caches, reducing // It runs a single consolidated cleanup goroutine for all caches, reducing
// goroutine count and CPU overhead compared to per-cache cleanup routines. // goroutine count and CPU overhead compared to per-cache cleanup routines.
type UniversalCacheManager struct { type UniversalCacheManager struct {
tokenCache *UniversalCache sharedBackend backends.CacheBackend
blacklistCache *UniversalCache ctx context.Context
metadataCache *UniversalCache tokenTypeCache *UniversalCache
jwkCache *UniversalCache jwkCache *UniversalCache
sessionCache *UniversalCache sessionCache *UniversalCache
introspectionCache *UniversalCache // OAuth 2.0 Token Introspection cache (RFC 7662) introspectionCache *UniversalCache
tokenTypeCache *UniversalCache // Cache for token type detection results tokenCache *UniversalCache
sharedBackend backends.CacheBackend // Shared backend (Redis) that should be closed by manager, not individual caches metadataCache *UniversalCache
mu sync.RWMutex
logger *Logger logger *Logger
blacklistCache *UniversalCache
// Consolidated cleanup management cancel context.CancelFunc
ctx context.Context wg sync.WaitGroup
cancel context.CancelFunc mu sync.RWMutex
wg sync.WaitGroup cleanupStarted bool
cleanupStarted bool
} }
var ( var (