package traefikoidc import ( "context" "fmt" "time" ) // TokenResilienceConfig centralizes resilience configuration for token operations type TokenResilienceConfig struct { MetadataCacheConfig MetadataCacheResilienceConfig RetryConfig RetryConfig CircuitBreakerConfig CircuitBreakerConfig CircuitBreakerEnabled bool RetryEnabled bool } // MetadataCacheResilienceConfig defines resilience settings for metadata cache type MetadataCacheResilienceConfig struct { 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 func DefaultTokenResilienceConfig() TokenResilienceConfig { return TokenResilienceConfig{ CircuitBreakerEnabled: true, CircuitBreakerConfig: CircuitBreakerConfig{ MaxFailures: 3, Timeout: 30 * time.Second, ResetTimeout: 15 * time.Second, }, RetryEnabled: true, RetryConfig: RetryConfig{ MaxAttempts: 3, InitialDelay: 250 * time.Millisecond, MaxDelay: 2 * time.Second, BackoffFactor: 2.0, EnableJitter: true, RetryableErrors: []string{ "connection refused", "timeout", "temporary failure", "network unreachable", "connection reset", "no route to host", }, }, MetadataCacheConfig: DefaultMetadataCacheResilienceConfig(), } } // DefaultMetadataCacheResilienceConfig returns the default metadata cache resilience configuration func DefaultMetadataCacheResilienceConfig() MetadataCacheResilienceConfig { return MetadataCacheResilienceConfig{ EnableProgressiveGracePeriod: true, InitialGracePeriod: 5 * time.Minute, ExtendedGracePeriod: 15 * time.Minute, MaxGracePeriod: 30 * time.Minute, SecurityCriticalMaxGracePeriod: 15 * time.Minute, // Allan's security limit SecurityCriticalFields: []string{ "jwks_uri", "authorization_endpoint", "token_endpoint", "revocation_endpoint", "end_session_endpoint", }, } } // TokenResilienceManager coordinates resilience mechanisms for token operations type TokenResilienceManager struct { errorRecoveryManager *ErrorRecoveryManager circuitBreaker *CircuitBreaker retryExecutor *RetryExecutor logger *Logger config TokenResilienceConfig } // NewTokenResilienceManager creates a new token resilience manager func NewTokenResilienceManager(config TokenResilienceConfig, logger *Logger) *TokenResilienceManager { manager := &TokenResilienceManager{ config: config, logger: logger, } // Initialize error recovery manager manager.errorRecoveryManager = NewErrorRecoveryManager(logger) // Initialize circuit breaker if enabled if config.CircuitBreakerEnabled { manager.circuitBreaker = NewCircuitBreaker(config.CircuitBreakerConfig, logger) } // Initialize retry executor if enabled if config.RetryEnabled { manager.retryExecutor = NewRetryExecutor(config.RetryConfig, logger) } return manager } // ExecuteTokenOperation executes a token operation with full resilience support. // singleUse marks operations whose request must not be re-sent after a timeout // (e.g. the authorization-code exchange, where the one-time code may already // have been consumed server-side) — only pre-send (connect) failures are retried. func (trm *TokenResilienceManager) ExecuteTokenOperation(ctx context.Context, operation string, singleUse bool, fn func() error) error { if trm.logger != nil { trm.logger.Debugf("Executing token operation %s with resilience", operation) } // If no resilience mechanisms are enabled, execute directly if !trm.config.CircuitBreakerEnabled && !trm.config.RetryEnabled { return fn() } // Compose resilience mechanisms. Retry is the INNER layer (around the // operation) and the circuit breaker is the OUTER layer, so the breaker // observes one aggregate result per logical operation rather than one // failure per retry attempt (which would trip it prematurely — e.g. // maxFailures=3 + 3 retries opening the breaker on a single brief // outage, failing the very next genuine request). R106. var finalOperation func() error = fn // Wrap with retry if enabled (innermost layer) if trm.config.RetryEnabled && trm.retryExecutor != nil { originalOp := finalOperation if singleUse { finalOperation = func() error { return trm.retryExecutor.ExecuteSingleUseWithContext(ctx, originalOp) } } else { finalOperation = func() error { return trm.retryExecutor.ExecuteWithContext(ctx, originalOp) } } } // Wrap with circuit breaker if enabled (outermost layer) if trm.config.CircuitBreakerEnabled && trm.circuitBreaker != nil { originalOp := finalOperation finalOperation = func() error { return trm.circuitBreaker.ExecuteWithContext(ctx, originalOp) } } err := finalOperation() if err != nil && trm.logger != nil { trm.logger.Errorf("Token operation %s failed after resilience mechanisms: %v", operation, err) } else if trm.logger != nil { trm.logger.Debugf("Token operation %s completed successfully", operation) } return err } // ExecuteTokenExchange executes token exchange with resilience func (trm *TokenResilienceManager) ExecuteTokenExchange(ctx context.Context, t *TraefikOidc, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) { var result *TokenResponse var err error operation := fmt.Sprintf("token_exchange_%s", grantType) err = trm.ExecuteTokenOperation(ctx, operation, grantType == "authorization_code", func() error { result, err = t.exchangeTokens(ctx, grantType, codeOrToken, redirectURL, codeVerifier) return err }) return result, err } // ExecuteTokenRefresh executes token refresh with resilience func (trm *TokenResilienceManager) ExecuteTokenRefresh(ctx context.Context, t *TraefikOidc, refreshToken string) (*TokenResponse, error) { var result *TokenResponse var err error // Refresh is single-use (like the auth-code exchange): a client-side timeout // may mean the provider already processed AND rotated the refresh token, so // re-presenting it on a retry surfaces invalid_grant and the session is // cleared — escalating a transient timeout into a full logout. Single-use // retries only on "never sent" errors (connect/dial), never on timeout. err = trm.ExecuteTokenOperation(ctx, "token_refresh", true, func() error { // Call exchangeTokens directly to avoid recursion back to getNewTokenWithRefreshToken // which would call ExecuteTokenRefresh again, causing infinite loop (issue #67) result, err = t.exchangeTokens(ctx, "refresh_token", refreshToken, "", "") return err }) return result, err } // GetMetrics returns metrics for all resilience mechanisms func (trm *TokenResilienceManager) GetMetrics() map[string]interface{} { metrics := make(map[string]interface{}) if trm.circuitBreaker != nil { metrics["circuit_breaker"] = trm.circuitBreaker.GetMetrics() } if trm.retryExecutor != nil { metrics["retry_executor"] = trm.retryExecutor.GetMetrics() } if trm.errorRecoveryManager != nil { recoveryMetrics := trm.errorRecoveryManager.GetRecoveryMetrics() metrics["error_recovery"] = recoveryMetrics } return metrics } // Reset resets all resilience mechanisms func (trm *TokenResilienceManager) Reset() { if trm.circuitBreaker != nil { trm.circuitBreaker.Reset() } if trm.retryExecutor != nil { trm.retryExecutor.Reset() } if trm.logger != nil { trm.logger.Debugf("Token resilience manager has been reset") } } // IsSecurityCriticalField checks if a metadata field is security-critical func (config MetadataCacheResilienceConfig) IsSecurityCriticalField(fieldName string) bool { for _, criticalField := range config.SecurityCriticalFields { if fieldName == criticalField { return true } } return false } // GetEffectiveMaxGracePeriod returns the effective maximum grace period for a field // considering Allan's security limits func (config MetadataCacheResilienceConfig) GetEffectiveMaxGracePeriod(fieldName string) time.Duration { if config.IsSecurityCriticalField(fieldName) { return config.SecurityCriticalMaxGracePeriod } return config.MaxGracePeriod }