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