Files
traefikoidc/token_resilience.go
lukaszraczylo 6efb78b7a8 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.
2025-12-12 18:35:06 +00:00

231 lines
7.0 KiB
Go

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
func (trm *TokenResilienceManager) ExecuteTokenOperation(ctx context.Context, operation string, 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
var finalOperation func() error = fn
// Wrap with circuit breaker if enabled
if trm.config.CircuitBreakerEnabled && trm.circuitBreaker != nil {
originalOp := finalOperation
finalOperation = func() error {
return trm.circuitBreaker.ExecuteWithContext(ctx, originalOp)
}
}
// Wrap with retry if enabled
if trm.config.RetryEnabled && trm.retryExecutor != nil {
originalOp := finalOperation
finalOperation = func() error {
return trm.retryExecutor.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, 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
err = trm.ExecuteTokenOperation(ctx, "token_refresh", 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
}