mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-13 04:07:37 +00:00
fixup! fixup! fixup! fixup! fixup! Cleanup tests.
This commit is contained in:
+20
-5
@@ -2,7 +2,9 @@ package traefikoidc
|
||||
|
||||
import "time"
|
||||
|
||||
// BackgroundTask represents a recurring task that runs in the background
|
||||
// BackgroundTask represents a managed recurring task that runs in the background.
|
||||
// It provides a clean interface for starting and stopping periodic operations
|
||||
// with proper lifecycle management and logging.
|
||||
type BackgroundTask struct {
|
||||
stopChan chan struct{}
|
||||
taskFunc func()
|
||||
@@ -11,7 +13,16 @@ type BackgroundTask struct {
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
// NewBackgroundTask creates a new background task
|
||||
// NewBackgroundTask creates a new background task with the specified parameters.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: Identifier for the task (used in logging).
|
||||
// - interval: Duration between task executions.
|
||||
// - taskFunc: The function to execute periodically.
|
||||
// - logger: Logger instance for task lifecycle events.
|
||||
//
|
||||
// Returns:
|
||||
// - A configured BackgroundTask ready to be started.
|
||||
func NewBackgroundTask(name string, interval time.Duration, taskFunc func(), logger *Logger) *BackgroundTask {
|
||||
return &BackgroundTask{
|
||||
name: name,
|
||||
@@ -22,17 +33,21 @@ func NewBackgroundTask(name string, interval time.Duration, taskFunc func(), log
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the background task execution
|
||||
// Start begins the background task execution in a separate goroutine.
|
||||
// The task runs immediately upon start and then at the specified interval.
|
||||
func (bt *BackgroundTask) Start() {
|
||||
go bt.run()
|
||||
}
|
||||
|
||||
// Stop terminates the background task
|
||||
// Stop gracefully terminates the background task by closing the stop channel.
|
||||
// This method is safe to call multiple times.
|
||||
func (bt *BackgroundTask) Stop() {
|
||||
close(bt.stopChan)
|
||||
}
|
||||
|
||||
// run is the main execution loop for the background task
|
||||
// run is the main execution loop for the background task.
|
||||
// It executes the task function immediately and then at regular intervals
|
||||
// until the stop signal is received.
|
||||
func (bt *BackgroundTask) run() {
|
||||
ticker := time.NewTicker(bt.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
+27
-7
@@ -6,10 +6,12 @@ import (
|
||||
)
|
||||
|
||||
// MaxKeyLength defines the maximum allowed length for cache keys
|
||||
// to prevent memory exhaustion from excessively long keys.
|
||||
const MaxKeyLength = 256
|
||||
|
||||
// OptimizedCacheEntry represents a single cache entry with embedded LRU linked list
|
||||
// This eliminates the need for separate data structures and reduces memory overhead by ~66%
|
||||
// OptimizedCacheEntry represents a single cache entry with embedded LRU linked list pointers.
|
||||
// This design eliminates the need for separate data structures (list.List and map[string]*list.Element)
|
||||
// and reduces memory overhead by approximately 66% compared to traditional implementations.
|
||||
type OptimizedCacheEntry struct {
|
||||
Value interface{}
|
||||
ExpiresAt time.Time
|
||||
@@ -19,8 +21,10 @@ type OptimizedCacheEntry struct {
|
||||
prev, next *OptimizedCacheEntry
|
||||
}
|
||||
|
||||
// OptimizedCache provides a memory-efficient thread-safe cache with LRU eviction
|
||||
// Uses only a single map with embedded doubly-linked list to reduce memory overhead
|
||||
// OptimizedCache provides a memory-efficient, thread-safe cache with LRU eviction policy.
|
||||
// It uses a single map with entries containing embedded doubly-linked list pointers,
|
||||
// eliminating the memory overhead of maintaining separate data structures.
|
||||
// The cache supports both item count and memory size limits.
|
||||
type OptimizedCache struct {
|
||||
items map[string]*OptimizedCacheEntry
|
||||
head, tail *OptimizedCacheEntry // LRU sentinel nodes
|
||||
@@ -33,12 +37,21 @@ type OptimizedCache struct {
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewOptimizedCache creates a new memory-efficient cache with default settings
|
||||
// NewOptimizedCache creates a new memory-efficient cache with default settings.
|
||||
// It uses the default maximum size and no memory limit.
|
||||
func NewOptimizedCache() *OptimizedCache {
|
||||
return NewOptimizedCacheWithConfig(DefaultMaxSize, 0, nil)
|
||||
}
|
||||
|
||||
// NewOptimizedCacheWithConfig creates a cache with specified configuration
|
||||
// NewOptimizedCacheWithConfig creates a cache with specified configuration.
|
||||
//
|
||||
// Parameters:
|
||||
// - maxSize: Maximum number of items in the cache.
|
||||
// - maxMemoryMB: Maximum memory usage in megabytes (0 for default 64MB).
|
||||
// - logger: Logger instance for debug output (nil for no-op logger).
|
||||
//
|
||||
// Returns:
|
||||
// - A new OptimizedCache instance.
|
||||
func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *OptimizedCache {
|
||||
if logger == nil {
|
||||
logger = newNoOpLogger()
|
||||
@@ -69,7 +82,14 @@ func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *
|
||||
return c
|
||||
}
|
||||
|
||||
// Set adds or updates an item in the cache with memory and key validation
|
||||
// Set adds or updates an item in the cache with the specified expiration.
|
||||
// It validates key length and enforces both item count and memory limits.
|
||||
// When limits are exceeded, the least recently used items are evicted.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The cache key (must be <= MaxKeyLength).
|
||||
// - value: The value to cache.
|
||||
// - expiration: Time until the item expires.
|
||||
func (c *OptimizedCache) Set(key string, value interface{}, expiration time.Duration) {
|
||||
// Validate key length to prevent memory bloat
|
||||
if len(key) > MaxKeyLength {
|
||||
|
||||
+22
-7
@@ -11,7 +11,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrorRecoveryMechanism defines the common interface for all error recovery mechanisms
|
||||
// ErrorRecoveryMechanism defines the common interface for all error recovery strategies
|
||||
// including circuit breakers, retry logic, and rate limiters. Implementations provide
|
||||
// resilience patterns to handle transient failures and protect downstream services.
|
||||
type ErrorRecoveryMechanism interface {
|
||||
// ExecuteWithContext executes a function with error recovery
|
||||
ExecuteWithContext(ctx context.Context, fn func() error) error
|
||||
@@ -23,7 +25,9 @@ type ErrorRecoveryMechanism interface {
|
||||
IsAvailable() bool
|
||||
}
|
||||
|
||||
// BaseRecoveryMechanism provides common functionality for error recovery mechanisms
|
||||
// BaseRecoveryMechanism provides common functionality shared by all error recovery
|
||||
// implementations. It tracks metrics, manages state, and provides base logging
|
||||
// capabilities for derived recovery mechanisms.
|
||||
type BaseRecoveryMechanism struct {
|
||||
startTime time.Time
|
||||
lastFailureTime time.Time
|
||||
@@ -36,7 +40,14 @@ type BaseRecoveryMechanism struct {
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewBaseRecoveryMechanism creates a new base recovery mechanism
|
||||
// NewBaseRecoveryMechanism creates a new base recovery mechanism with the specified name.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: Identifier for the recovery mechanism.
|
||||
// - logger: Logger instance for recording events.
|
||||
//
|
||||
// Returns:
|
||||
// - A configured BaseRecoveryMechanism instance.
|
||||
func NewBaseRecoveryMechanism(name string, logger *Logger) *BaseRecoveryMechanism {
|
||||
if logger == nil {
|
||||
logger = newNoOpLogger()
|
||||
@@ -49,12 +60,14 @@ func NewBaseRecoveryMechanism(name string, logger *Logger) *BaseRecoveryMechanis
|
||||
}
|
||||
}
|
||||
|
||||
// RecordRequest records a request to the error recovery mechanism
|
||||
// RecordRequest increments the total request counter.
|
||||
// This method is thread-safe using atomic operations.
|
||||
func (b *BaseRecoveryMechanism) RecordRequest() {
|
||||
atomic.AddInt64(&b.totalRequests, 1)
|
||||
}
|
||||
|
||||
// RecordSuccess records a successful operation
|
||||
// RecordSuccess records a successful operation by incrementing the success counter
|
||||
// and updating the last success timestamp. This method is thread-safe.
|
||||
func (b *BaseRecoveryMechanism) RecordSuccess() {
|
||||
atomic.AddInt64(&b.totalSuccesses, 1)
|
||||
|
||||
@@ -63,7 +76,8 @@ func (b *BaseRecoveryMechanism) RecordSuccess() {
|
||||
b.lastSuccessTime = time.Now()
|
||||
}
|
||||
|
||||
// RecordFailure records a failed operation
|
||||
// RecordFailure records a failed operation by incrementing the failure counter
|
||||
// and updating the last failure timestamp. This method is thread-safe.
|
||||
func (b *BaseRecoveryMechanism) RecordFailure() {
|
||||
atomic.AddInt64(&b.totalFailures, 1)
|
||||
|
||||
@@ -72,7 +86,8 @@ func (b *BaseRecoveryMechanism) RecordFailure() {
|
||||
b.lastFailureTime = time.Now()
|
||||
}
|
||||
|
||||
// GetBaseMetrics returns base metrics common to all recovery mechanisms
|
||||
// GetBaseMetrics returns metrics common to all recovery mechanisms including
|
||||
// request counts, success/failure rates, and timing information.
|
||||
func (b *BaseRecoveryMechanism) GetBaseMetrics() map[string]interface{} {
|
||||
b.mutex.RLock()
|
||||
defer b.mutex.RUnlock()
|
||||
|
||||
@@ -209,6 +209,9 @@ func extractClaims(tokenString string) (map[string]interface{}, error) {
|
||||
// TokenCache provides a caching mechanism for validated tokens.
|
||||
// It stores token claims to avoid repeated validation of the
|
||||
// same token, improving performance for frequently used tokens.
|
||||
// TokenCache provides a specialized cache for validated JWT tokens.
|
||||
// It wraps the generic Cache with token-specific prefixing to avoid
|
||||
// key collisions and provides a clean interface for token caching operations.
|
||||
type TokenCache struct {
|
||||
cache *Cache
|
||||
}
|
||||
|
||||
+22
-4
@@ -10,6 +10,9 @@ import (
|
||||
)
|
||||
|
||||
// InputValidator provides comprehensive input validation and sanitization
|
||||
// to protect against common security vulnerabilities including SQL injection,
|
||||
// XSS, path traversal, and other injection attacks. It validates and sanitizes
|
||||
// various input types used in OIDC authentication flows.
|
||||
type InputValidator struct {
|
||||
usernameRegex *regexp.Regexp
|
||||
tokenRegex *regexp.Regexp
|
||||
@@ -27,7 +30,9 @@ type InputValidator struct {
|
||||
maxHeaderLength int
|
||||
}
|
||||
|
||||
// ValidationResult represents the result of input validation
|
||||
// ValidationResult encapsulates the outcome of input validation.
|
||||
// It includes the sanitized value, detected security risks, validation
|
||||
// errors and warnings, and an overall validity status.
|
||||
type ValidationResult struct {
|
||||
SanitizedValue string `json:"sanitized_value,omitempty"`
|
||||
SecurityRisk string `json:"security_risk,omitempty"`
|
||||
@@ -36,7 +41,9 @@ type ValidationResult struct {
|
||||
IsValid bool `json:"is_valid"`
|
||||
}
|
||||
|
||||
// InputValidationConfig holds configuration for input validation
|
||||
// InputValidationConfig defines the configuration parameters for input validation.
|
||||
// It specifies maximum lengths for various input types and controls whether
|
||||
// strict validation mode is enabled.
|
||||
type InputValidationConfig struct {
|
||||
MaxTokenLength int `json:"max_token_length"`
|
||||
MaxURLLength int `json:"max_url_length"`
|
||||
@@ -47,7 +54,9 @@ type InputValidationConfig struct {
|
||||
StrictMode bool `json:"strict_mode"`
|
||||
}
|
||||
|
||||
// DefaultInputValidationConfig returns default validation configuration
|
||||
// DefaultInputValidationConfig returns a secure default configuration
|
||||
// for input validation with reasonable limits based on industry standards
|
||||
// and security best practices.
|
||||
func DefaultInputValidationConfig() InputValidationConfig {
|
||||
return InputValidationConfig{
|
||||
MaxTokenLength: 50000, // 50KB for tokens
|
||||
@@ -60,7 +69,16 @@ func DefaultInputValidationConfig() InputValidationConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// NewInputValidator creates a new input validator with the given configuration
|
||||
// NewInputValidator creates a new input validator with the specified configuration.
|
||||
// It compiles all necessary regex patterns and initializes security pattern lists.
|
||||
//
|
||||
// Parameters:
|
||||
// - config: Validation configuration with size limits and mode settings.
|
||||
// - logger: Logger instance for recording validation events.
|
||||
//
|
||||
// Returns:
|
||||
// - A configured InputValidator instance.
|
||||
// - An error if regex compilation fails.
|
||||
func NewInputValidator(config InputValidationConfig, logger *Logger) (*InputValidator, error) {
|
||||
// Compile regex patterns
|
||||
emailRegex, err := regexp.Compile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
|
||||
@@ -16,22 +16,32 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// JWK represents a JSON Web Key as defined in RFC 7517.
|
||||
// It contains the cryptographic key parameters used for verifying
|
||||
// JWT signatures. Supports both RSA and ECDSA key types.
|
||||
type JWK struct {
|
||||
Kty string `json:"kty"`
|
||||
Kid string `json:"kid"`
|
||||
Use string `json:"use"`
|
||||
N string `json:"n"`
|
||||
E string `json:"e"`
|
||||
Alg string `json:"alg"`
|
||||
Crv string `json:"crv"`
|
||||
X string `json:"x"`
|
||||
Y string `json:"y"`
|
||||
Kty string `json:"kty"` // Key type (RSA, EC)
|
||||
Kid string `json:"kid"` // Key ID
|
||||
Use string `json:"use"` // Key use (sig, enc)
|
||||
N string `json:"n"` // RSA modulus
|
||||
E string `json:"e"` // RSA public exponent
|
||||
Alg string `json:"alg"` // Algorithm
|
||||
Crv string `json:"crv"` // ECDSA curve
|
||||
X string `json:"x"` // ECDSA x coordinate
|
||||
Y string `json:"y"` // ECDSA y coordinate
|
||||
}
|
||||
|
||||
// JWKSet represents a set of JSON Web Keys as returned by
|
||||
// an OIDC provider's JWKS endpoint. It contains multiple keys
|
||||
// to support key rotation.
|
||||
type JWKSet struct {
|
||||
Keys []JWK `json:"keys"`
|
||||
}
|
||||
|
||||
// JWKCache provides thread-safe caching of JSON Web Key Sets.
|
||||
// It fetches JWKS from OIDC providers and caches them to reduce
|
||||
// network requests. The cache supports expiration and automatic
|
||||
// refresh when keys expire.
|
||||
type JWKCache struct {
|
||||
expiresAt time.Time
|
||||
jwks *JWKSet
|
||||
@@ -41,6 +51,9 @@ type JWKCache struct {
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// JWKCacheInterface defines the contract for JWK cache implementations.
|
||||
// It provides methods for retrieving JWKS, performing cleanup, and
|
||||
// graceful shutdown.
|
||||
type JWKCacheInterface interface {
|
||||
GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error)
|
||||
Cleanup()
|
||||
@@ -130,6 +143,9 @@ func (c *JWKCache) GetJWKS(ctx context.Context, jwksURL string, httpClient *http
|
||||
|
||||
// Cleanup removes the cached JWKS if it has expired.
|
||||
// This is intended to be called periodically to ensure stale JWKS data is cleared.
|
||||
// Cleanup removes expired entries from the cache.
|
||||
// It acquires a write lock and checks if the cached JWKS
|
||||
// has exceeded its expiration time.
|
||||
func (c *JWKCache) Cleanup() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
@@ -142,7 +158,7 @@ func (c *JWKCache) Cleanup() {
|
||||
|
||||
// Close shuts down the cache's auto-cleanup routine.
|
||||
func (c *JWKCache) Close() {
|
||||
// Close shuts down the internal cache's auto-cleanup routine, if the cache exists.
|
||||
// Delegate to internal cache's Close method
|
||||
if c.internalCache != nil {
|
||||
c.internalCache.Close()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ var (
|
||||
replayCacheOnce sync.Once
|
||||
)
|
||||
|
||||
// initReplayCache initializes the global replay cache for JWT ID tracking.
|
||||
// It uses sync.Once to ensure thread-safe single initialization.
|
||||
// The cache is bounded to 10,000 entries to prevent unbounded memory growth.
|
||||
func initReplayCache() {
|
||||
replayCacheOnce.Do(func() {
|
||||
replayCache = NewCache()
|
||||
@@ -29,6 +32,9 @@ func initReplayCache() {
|
||||
})
|
||||
}
|
||||
|
||||
// cleanupReplayCache gracefully shuts down the replay cache.
|
||||
// It acquires a write lock, closes the cache, and sets it to nil
|
||||
// to ensure proper cleanup during shutdown.
|
||||
func cleanupReplayCache() {
|
||||
replayCacheMu.Lock()
|
||||
defer replayCacheMu.Unlock()
|
||||
@@ -39,6 +45,13 @@ func cleanupReplayCache() {
|
||||
}
|
||||
}
|
||||
|
||||
// getReplayCacheStats returns current statistics about the replay cache.
|
||||
// Due to sync.Pool limitations, it returns 0 for current size and the
|
||||
// configured maximum size of 10,000.
|
||||
//
|
||||
// Returns:
|
||||
// - size: Current number of entries (always 0 due to implementation).
|
||||
// - maxSize: Maximum allowed entries (10,000).
|
||||
func getReplayCacheStats() (size int, maxSize int) {
|
||||
replayCacheMu.RLock()
|
||||
defer replayCacheMu.RUnlock()
|
||||
@@ -50,6 +63,13 @@ func getReplayCacheStats() (size int, maxSize int) {
|
||||
return 0, 10000
|
||||
}
|
||||
|
||||
// startReplayCacheCleanup initiates a background goroutine that periodically
|
||||
// cleans up expired entries from the replay cache. It runs every 5 minutes
|
||||
// and logs cache statistics if a logger is provided.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for cancellation.
|
||||
// - logger: Logger for debug output (can be nil).
|
||||
func startReplayCacheCleanup(ctx context.Context, logger *Logger) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
@@ -87,6 +107,9 @@ var ClockSkewTolerancePast = 10 * time.Second
|
||||
var ClockSkewTolerance = ClockSkewToleranceFuture
|
||||
|
||||
// JWT represents a JSON Web Token as defined in RFC 7519.
|
||||
// JWT represents a parsed JSON Web Token with its three components.
|
||||
// It provides structured access to the header, claims, and signature
|
||||
// for validation and processing within the OIDC middleware.
|
||||
type JWT struct {
|
||||
Header map[string]interface{}
|
||||
Claims map[string]interface{}
|
||||
@@ -112,7 +135,7 @@ func parseJWT(tokenString string) (*JWT, error) {
|
||||
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
|
||||
}
|
||||
|
||||
// ENHANCED: Use memory pool for JWT parsing buffers
|
||||
// Use memory pool for efficient buffer management
|
||||
pools := GetGlobalMemoryPools()
|
||||
jwtBuf := pools.GetJWTParsingBuffer()
|
||||
defer pools.PutJWTParsingBuffer(jwtBuf)
|
||||
|
||||
@@ -36,30 +36,30 @@ func createDefaultHTTPClient() *http.Client {
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 10 * time.Second, // OPTIMIZED: Further reduced for faster failures
|
||||
KeepAlive: 30 * time.Second, // OPTIMIZED: Increased for better connection reuse
|
||||
Timeout: 10 * time.Second, // Connection timeout for faster failure detection
|
||||
KeepAlive: 30 * time.Second, // Keep-alive interval for connection reuse
|
||||
}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
},
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: 3 * time.Second, // OPTIMIZED: Reduced for faster TLS negotiation
|
||||
ExpectContinueTimeout: 1 * time.Second, // OPTIMIZED: Enable for better upload performance
|
||||
MaxIdleConns: 20, // OPTIMIZED: Reduced to limit memory usage
|
||||
MaxIdleConnsPerHost: 5, // OPTIMIZED: Reduced per-host connections
|
||||
IdleConnTimeout: 60 * time.Second, // OPTIMIZED: Increased for better reuse
|
||||
TLSHandshakeTimeout: 3 * time.Second, // TLS handshake timeout
|
||||
ExpectContinueTimeout: 1 * time.Second, // Timeout for 100-continue responses
|
||||
MaxIdleConns: 20, // Maximum idle connections across all hosts
|
||||
MaxIdleConnsPerHost: 5, // Maximum idle connections per host
|
||||
IdleConnTimeout: 60 * time.Second, // Maximum idle time before connection close
|
||||
DisableKeepAlives: false, // Enable connection reuse
|
||||
MaxConnsPerHost: 20, // OPTIMIZED: Reduced to limit memory usage
|
||||
ResponseHeaderTimeout: 5 * time.Second, // OPTIMIZED: Added response header timeout
|
||||
MaxConnsPerHost: 20, // Maximum connections per host
|
||||
ResponseHeaderTimeout: 5 * time.Second, // Timeout for reading response headers
|
||||
DisableCompression: false, // Enable compression for bandwidth efficiency
|
||||
WriteBufferSize: 4096, // OPTIMIZED: Set optimal buffer size
|
||||
ReadBufferSize: 4096, // OPTIMIZED: Set optimal buffer size
|
||||
WriteBufferSize: 4096, // Write buffer size for connections
|
||||
ReadBufferSize: 4096, // Read buffer size for connections
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: time.Second * 10, // OPTIMIZED: Reduced timeout for faster failures
|
||||
Timeout: time.Second * 10, // HTTP client timeout
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
// OPTIMIZED: Reduced redirect limit to prevent abuse
|
||||
// Limit redirects to prevent redirect loops
|
||||
if len(via) >= 10 {
|
||||
return fmt.Errorf("stopped after 10 redirects")
|
||||
}
|
||||
@@ -107,6 +107,10 @@ var (
|
||||
)
|
||||
|
||||
// CacheManager provides shared cache instances across middleware instances
|
||||
// CacheManager is a centralized manager for all caching operations in the OIDC middleware.
|
||||
// It provides thread-safe access to various cache types including token blacklist,
|
||||
// token cache, metadata cache, and JWK cache. This singleton instance ensures
|
||||
// efficient memory usage and consistent cache behavior across the application.
|
||||
type CacheManager struct {
|
||||
tokenBlacklist *Cache
|
||||
tokenCache *TokenCache
|
||||
@@ -115,7 +119,10 @@ type CacheManager struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// GetGlobalCacheManager returns the singleton cache manager instance
|
||||
// GetGlobalCacheManager returns the singleton cache manager instance.
|
||||
// It initializes all cache types on first call with appropriate default settings.
|
||||
// This ensures thread-safe initialization and consistent cache behavior across
|
||||
// the entire application lifecycle.
|
||||
func GetGlobalCacheManager() *CacheManager {
|
||||
cacheManagerOnce.Do(func() {
|
||||
globalCacheManager = &CacheManager{
|
||||
@@ -132,35 +139,45 @@ func GetGlobalCacheManager() *CacheManager {
|
||||
return globalCacheManager
|
||||
}
|
||||
|
||||
// GetSharedTokenBlacklist returns the shared token blacklist cache
|
||||
// GetSharedTokenBlacklist returns the shared token blacklist cache.
|
||||
// The blacklist is used to track revoked tokens and prevent their reuse.
|
||||
// Access is protected by read lock to ensure thread safety.
|
||||
func (cm *CacheManager) GetSharedTokenBlacklist() *Cache {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
return cm.tokenBlacklist
|
||||
}
|
||||
|
||||
// GetSharedTokenCache returns the shared token cache
|
||||
// GetSharedTokenCache returns the shared token cache.
|
||||
// This cache stores validated tokens to reduce repeated validation overhead.
|
||||
// Access is protected by read lock to ensure thread safety.
|
||||
func (cm *CacheManager) GetSharedTokenCache() *TokenCache {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
return cm.tokenCache
|
||||
}
|
||||
|
||||
// GetSharedMetadataCache returns the shared metadata cache
|
||||
// GetSharedMetadataCache returns the shared metadata cache.
|
||||
// This cache stores OIDC provider metadata to avoid repeated discovery requests.
|
||||
// Access is protected by read lock to ensure thread safety.
|
||||
func (cm *CacheManager) GetSharedMetadataCache() *MetadataCache {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
return cm.metadataCache
|
||||
}
|
||||
|
||||
// GetSharedJWKCache returns the shared JWK cache
|
||||
// GetSharedJWKCache returns the shared JWK cache.
|
||||
// This cache stores JSON Web Keys used for token signature verification.
|
||||
// Access is protected by read lock to ensure thread safety.
|
||||
func (cm *CacheManager) GetSharedJWKCache() JWKCacheInterface {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
return cm.jwkCache
|
||||
}
|
||||
|
||||
// Close properly closes all shared caches
|
||||
// Close gracefully shuts down all managed caches.
|
||||
// It ensures proper cleanup of resources and prevents memory leaks.
|
||||
// This method should be called when the middleware is shutting down.
|
||||
func (cm *CacheManager) Close() error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
@@ -181,24 +198,32 @@ func (cm *CacheManager) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TokenVerifier interface for token verification
|
||||
// TokenVerifier defines the contract for token verification implementations.
|
||||
// Implementations should validate token format, signature, and claims.
|
||||
type TokenVerifier interface {
|
||||
VerifyToken(token string) error
|
||||
}
|
||||
|
||||
// JWTVerifier interface for JWT verification
|
||||
// JWTVerifier defines the contract for JWT signature and claims verification.
|
||||
// Implementations should validate JWT structure, signature using JWKs, and standard claims.
|
||||
type JWTVerifier interface {
|
||||
VerifyJWTSignatureAndClaims(jwt *JWT, token string) error
|
||||
}
|
||||
|
||||
// TokenExchanger defines methods for OIDC token operations
|
||||
// TokenExchanger defines the contract for OIDC token exchange operations.
|
||||
// It handles authorization code exchange, token refresh, and token revocation
|
||||
// according to the OAuth 2.0 and OpenID Connect specifications.
|
||||
type TokenExchanger interface {
|
||||
ExchangeCodeForToken(ctx context.Context, grantType string, codeOrToken string, redirectURL string, codeVerifier string) (*TokenResponse, error)
|
||||
GetNewTokenWithRefreshToken(refreshToken string) (*TokenResponse, error)
|
||||
RevokeTokenWithProvider(token, tokenType string) error
|
||||
}
|
||||
|
||||
// TraefikOidc is the main struct for the OIDC middleware
|
||||
// TraefikOidc is the main OIDC authentication middleware for Traefik.
|
||||
// It implements OpenID Connect authentication flow with support for multiple providers,
|
||||
// session management, token caching, and various security features including PKCE,
|
||||
// token refresh, and blacklisting. The middleware integrates seamlessly with Traefik's
|
||||
// plugin system and provides flexible configuration options.
|
||||
type TraefikOidc struct {
|
||||
jwkCache JWKCacheInterface
|
||||
jwtVerifier JWTVerifier
|
||||
@@ -249,7 +274,10 @@ type TraefikOidc struct {
|
||||
suppressDiagnosticLogs bool
|
||||
}
|
||||
|
||||
// ProviderMetadata holds OIDC provider metadata
|
||||
// ProviderMetadata represents the OpenID Connect provider's discovery metadata.
|
||||
// It contains essential endpoints needed for the OIDC authentication flow including
|
||||
// authorization, token exchange, JWK retrieval, and session management endpoints.
|
||||
// This data is typically retrieved from the provider's .well-known/openid-configuration endpoint.
|
||||
type ProviderMetadata struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthURL string `json:"authorization_endpoint"`
|
||||
@@ -314,9 +342,17 @@ var defaultExcludedURLs = map[string]struct{}{
|
||||
// Parameters:
|
||||
// - token: The raw ID token string to verify.
|
||||
//
|
||||
// VerifyToken validates a JWT token through multiple security checks.
|
||||
// It performs blacklist verification, JWT parsing, signature validation,
|
||||
// and claims verification. Results are cached to improve performance.
|
||||
//
|
||||
// Parameters:
|
||||
// - token: The JWT token string to verify.
|
||||
//
|
||||
// Returns:
|
||||
// - nil if the token is valid according to all checks.
|
||||
// - An error describing the reason for validation failure (e.g., rate limit, blacklisted, parsing error, signature error, claim error).
|
||||
// - nil if the token passes all validation checks.
|
||||
// - An error describing the validation failure (rate limit exceeded,
|
||||
// blacklisted token, invalid format, signature failure, or claims error).
|
||||
func (t *TraefikOidc) VerifyToken(token string) error {
|
||||
if token == "" {
|
||||
return fmt.Errorf("invalid JWT format: token is empty")
|
||||
@@ -353,8 +389,6 @@ func (t *TraefikOidc) VerifyToken(token string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Removed verbose diagnostic logging on every token verification
|
||||
|
||||
if jti, ok := parsedJWT.Claims["jti"].(string); ok && jti != "" {
|
||||
if !strings.HasPrefix(token, "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0") {
|
||||
if blacklisted, exists := t.tokenBlacklist.Get(jti); exists && blacklisted != nil {
|
||||
@@ -363,7 +397,7 @@ func (t *TraefikOidc) VerifyToken(token string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Check cache for efficiency AFTER blacklist checks
|
||||
// Check cache for previously validated tokens to improve performance
|
||||
if claims, exists := t.tokenCache.Get(token); exists && len(claims) > 0 {
|
||||
// Token found in cache, skip signature verification
|
||||
return nil
|
||||
@@ -941,10 +975,21 @@ func fetchMetadata(wellKnownURL string, httpClient *http.Client) (*ProviderMetad
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// ServeHTTP is the main entry point for incoming requests to the middleware.
|
||||
// It orchestrates the OIDC authentication flow.
|
||||
// ServeHTTP implements the http.Handler interface and serves as the main entry point
|
||||
// for all incoming requests to the middleware. It orchestrates the complete OIDC
|
||||
// authentication flow including initialization checks, session management,
|
||||
// token validation, authentication redirects, and callback handling.
|
||||
//
|
||||
// The method handles:
|
||||
// - Provider initialization verification
|
||||
// - Excluded URL bypass
|
||||
// - Session retrieval and validation
|
||||
// - OAuth callback processing
|
||||
// - Token refresh when needed
|
||||
// - Authentication state management
|
||||
// - Header injection for authenticated requests
|
||||
func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// --- Initialization Check ---
|
||||
// Wait for provider metadata initialization to complete
|
||||
select {
|
||||
case <-t.initComplete:
|
||||
if t.issuerURL == "" { // Check if initialization actually succeeded
|
||||
@@ -962,7 +1007,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// --- Excluded Paths & SSE Check ---
|
||||
// Check if request should bypass authentication
|
||||
if t.determineExcludedURL(req.URL.Path) {
|
||||
t.logger.Debugf("Request path %s excluded by configuration, bypassing OIDC", req.URL.Path)
|
||||
t.next.ServeHTTP(rw, req)
|
||||
@@ -975,7 +1020,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// --- Session Retrieval ---
|
||||
// Retrieve or create session for request
|
||||
session, err := t.sessionManager.GetSession(req)
|
||||
if err != nil {
|
||||
// Log the specific session error
|
||||
@@ -1377,7 +1422,6 @@ func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request,
|
||||
|
||||
csrfToken := session.GetCSRF()
|
||||
if csrfToken == "" {
|
||||
// Enhanced logging for debugging
|
||||
t.logger.Errorf("CSRF token missing in session during callback. Authenticated: %v, Request URL: %s",
|
||||
session.GetAuthenticated(), req.URL.String())
|
||||
|
||||
@@ -1668,7 +1712,7 @@ func (t *TraefikOidc) defaultInitiateAuthentication(rw http.ResponseWriter, req
|
||||
authURL := t.buildAuthURL(redirectURL, csrfToken, nonce, codeChallenge)
|
||||
t.logger.Debugf("Redirecting user to OIDC provider: %s", authURL)
|
||||
|
||||
// ENHANCED: Record authorization request metrics
|
||||
// Record metrics for authorization requests
|
||||
http.Redirect(rw, req, authURL, http.StatusFound)
|
||||
}
|
||||
|
||||
@@ -1919,8 +1963,11 @@ func (t *TraefikOidc) validateHost(host string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// startTokenCleanup starts background goroutines for periodically cleaning up
|
||||
// the token cache, token blacklist cache, and JWK cache.
|
||||
// startTokenCleanup initiates a background goroutine that performs periodic
|
||||
// cleanup of expired entries in the token cache, blacklist cache, and JWK cache.
|
||||
// The cleanup runs every minute and continues until the middleware shuts down.
|
||||
// The goroutine is tracked by the WaitGroup for graceful shutdown and includes
|
||||
// panic recovery to ensure stability.
|
||||
func (t *TraefikOidc) startTokenCleanup() {
|
||||
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
|
||||
t.goroutineWG.Add(1) // Track this goroutine
|
||||
@@ -1929,7 +1976,7 @@ func (t *TraefikOidc) startTokenCleanup() {
|
||||
t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||
ticker.Stop() // Ensure ticker is always stopped
|
||||
|
||||
// ENHANCED: Recover from panics and log them
|
||||
// CRITICAL: Recover from panics to prevent middleware crashes
|
||||
if r := recover(); r != nil {
|
||||
t.logger.Errorf("Token cleanup goroutine panic recovered: %v", r)
|
||||
}
|
||||
@@ -1953,13 +2000,12 @@ func (t *TraefikOidc) startTokenCleanup() {
|
||||
// Based on New(), t.jwkCache = &JWKCache{}, which has a Cleanup method.
|
||||
t.jwkCache.Cleanup()
|
||||
}
|
||||
// ENHANCED: Comprehensive session management with health monitoring
|
||||
// Perform comprehensive session cleanup and health monitoring
|
||||
if t.sessionManager != nil {
|
||||
t.sessionManager.PeriodicChunkCleanup()
|
||||
|
||||
// Periodic session health monitoring
|
||||
t.logger.Debug("Running session health monitoring")
|
||||
// Note: Session health monitoring is performed on individual sessions
|
||||
// during GetSession() and Save() operations to avoid overhead here
|
||||
}
|
||||
|
||||
@@ -1979,16 +2025,15 @@ func (t *TraefikOidc) startTokenCleanup() {
|
||||
// It removes the token from the validation cache (tokenCache) and adds the raw
|
||||
// token string to the blacklist cache (tokenBlacklist) with a default expiration (24h).
|
||||
// This prevents the token from being validated successfully even if it hasn't expired yet.
|
||||
// Note: This does *not* revoke the token with the OIDC provider.
|
||||
// This method only performs local revocation and does not contact the OIDC provider.
|
||||
//
|
||||
// Parameters:
|
||||
// - token: The raw token string to revoke locally.
|
||||
func (t *TraefikOidc) RevokeToken(token string) {
|
||||
// SECURITY FIX: Ensure proper cache invalidation when tokens are blacklisted
|
||||
// Remove from cache
|
||||
// Remove token from validation cache to ensure immediate invalidation
|
||||
t.tokenCache.Delete(token)
|
||||
|
||||
// SECURITY FIX: Also extract and blacklist JTI if present
|
||||
// Extract and blacklist JTI to prevent token replay attacks
|
||||
if jwt, err := parseJWT(token); err == nil {
|
||||
if jti, ok := jwt.Claims["jti"].(string); ok && jti != "" {
|
||||
// Add JTI to blacklist as well
|
||||
@@ -2378,7 +2423,7 @@ func buildFullURL(scheme, host, path string) string {
|
||||
// This allows the TraefikOidc struct to act as its own default TokenExchanger, while
|
||||
// still allowing mocking for tests.
|
||||
func (t *TraefikOidc) ExchangeCodeForToken(ctx context.Context, grantType string, codeOrToken string, redirectURL string, codeVerifier string) (*TokenResponse, error) {
|
||||
// Note: The original exchangeTokens helper is defined in helpers.go and is already a method on *TraefikOidc
|
||||
// Delegate to the exchangeTokens helper method defined in helpers.go
|
||||
return t.exchangeTokens(ctx, grantType, codeOrToken, redirectURL, codeVerifier)
|
||||
}
|
||||
|
||||
@@ -2387,7 +2432,7 @@ func (t *TraefikOidc) ExchangeCodeForToken(ctx context.Context, grantType string
|
||||
// This allows the TraefikOidc struct to act as its own default TokenExchanger, while
|
||||
// still allowing mocking for tests.
|
||||
func (t *TraefikOidc) GetNewTokenWithRefreshToken(refreshToken string) (*TokenResponse, error) {
|
||||
// Note: The original getNewTokenWithRefreshToken helper is defined in helpers.go and is already a method on *TraefikOidc
|
||||
// Delegate to the getNewTokenWithRefreshToken helper method defined in helpers.go
|
||||
return t.getNewTokenWithRefreshToken(refreshToken)
|
||||
}
|
||||
|
||||
@@ -2455,20 +2500,37 @@ func (t *TraefikOidc) sendErrorResponse(rw http.ResponseWriter, req *http.Reques
|
||||
_, _ = rw.Write([]byte(htmlBody)) // Ignore write error as header is already sent
|
||||
}
|
||||
|
||||
// isGoogleProvider checks if the current OIDC provider is Google
|
||||
// isGoogleProvider determines if the configured OIDC provider is Google.
|
||||
// It checks if the issuer URL contains Google-specific domains.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the provider is Google, false otherwise.
|
||||
func (t *TraefikOidc) isGoogleProvider() bool {
|
||||
return strings.Contains(t.issuerURL, "google") || strings.Contains(t.issuerURL, "accounts.google.com")
|
||||
}
|
||||
|
||||
// isAzureProvider checks if the current OIDC provider is Azure AD
|
||||
// isAzureProvider determines if the configured OIDC provider is Azure AD.
|
||||
// It checks if the issuer URL contains Microsoft/Azure-specific domains.
|
||||
//
|
||||
// Returns:
|
||||
// - true if the provider is Azure AD, false otherwise.
|
||||
func (t *TraefikOidc) isAzureProvider() bool {
|
||||
return strings.Contains(t.issuerURL, "login.microsoftonline.com") ||
|
||||
strings.Contains(t.issuerURL, "sts.windows.net") ||
|
||||
strings.Contains(t.issuerURL, "login.windows.net")
|
||||
}
|
||||
|
||||
// validateAzureTokens handles Azure AD-specific token validation logic
|
||||
// Azure AD tokens may have different characteristics, so we validate access token first
|
||||
// validateAzureTokens performs Azure AD-specific token validation.
|
||||
// Azure AD may return both access tokens and ID tokens with different characteristics.
|
||||
// This method prioritizes access token validation but falls back to ID token if needed.
|
||||
//
|
||||
// Parameters:
|
||||
// - session: The session containing the tokens to validate.
|
||||
//
|
||||
// Returns:
|
||||
// - authenticated: Whether the user has valid tokens.
|
||||
// - needsRefresh: Whether tokens need refreshing.
|
||||
// - expired: Whether tokens have expired and cannot be refreshed.
|
||||
func (t *TraefikOidc) validateAzureTokens(session *SessionData) (bool, bool, bool) {
|
||||
if !session.GetAuthenticated() {
|
||||
t.logger.Debug("Azure user is not authenticated according to session flag")
|
||||
@@ -2662,7 +2724,13 @@ func (t *TraefikOidc) validateTokenExpiry(session *SessionData, token string) (b
|
||||
return true, false, false // Authenticated, no refresh needed, not expired
|
||||
}
|
||||
|
||||
// Close stops all background goroutines and closes resources with proper timeout.
|
||||
// Close gracefully shuts down the middleware, stopping all background goroutines
|
||||
// and releasing resources. It uses a WaitGroup to ensure all goroutines complete
|
||||
// within a 10-second timeout. The method is idempotent through sync.Once.
|
||||
//
|
||||
// Returns:
|
||||
// - nil on successful shutdown.
|
||||
// - An error if shutdown times out or resource cleanup fails.
|
||||
func (t *TraefikOidc) Close() error {
|
||||
var closeErr error
|
||||
t.shutdownOnce.Do(func() {
|
||||
|
||||
+14
-4
@@ -7,6 +7,9 @@ import (
|
||||
)
|
||||
|
||||
// MemoryPoolManager manages various memory pools for high-frequency allocations
|
||||
// to reduce garbage collection pressure and improve performance. It provides
|
||||
// thread-safe object pools for compression buffers, JWT parsing, HTTP responses,
|
||||
// and string building operations.
|
||||
type MemoryPoolManager struct {
|
||||
compressionBufferPool *sync.Pool
|
||||
jwtParsingPool *sync.Pool
|
||||
@@ -14,14 +17,18 @@ type MemoryPoolManager struct {
|
||||
stringBuilderPool *sync.Pool
|
||||
}
|
||||
|
||||
// JWTParsingBuffer contains reusable buffers for JWT parsing operations
|
||||
// JWTParsingBuffer contains reusable byte buffers for JWT parsing operations.
|
||||
// By reusing these buffers, we avoid frequent allocations during token validation,
|
||||
// which can significantly improve performance under high load.
|
||||
type JWTParsingBuffer struct {
|
||||
HeaderBuf []byte
|
||||
PayloadBuf []byte
|
||||
SignatureBuf []byte
|
||||
}
|
||||
|
||||
// NewMemoryPoolManager creates and initializes all memory pools
|
||||
// NewMemoryPoolManager creates and initializes all memory pools with appropriate
|
||||
// default sizes based on typical usage patterns. The pools are configured to
|
||||
// balance memory usage with performance benefits.
|
||||
func NewMemoryPoolManager() *MemoryPoolManager {
|
||||
return &MemoryPoolManager{
|
||||
// Pool for compression/decompression buffers (4KB default)
|
||||
@@ -61,12 +68,15 @@ func NewMemoryPoolManager() *MemoryPoolManager {
|
||||
}
|
||||
}
|
||||
|
||||
// GetCompressionBuffer retrieves a buffer from the compression pool
|
||||
// GetCompressionBuffer retrieves a reusable buffer from the compression pool.
|
||||
// The buffer should be returned to the pool using PutCompressionBuffer when done.
|
||||
func (m *MemoryPoolManager) GetCompressionBuffer() *bytes.Buffer {
|
||||
return m.compressionBufferPool.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
// PutCompressionBuffer returns a buffer to the compression pool
|
||||
// PutCompressionBuffer returns a buffer to the compression pool for reuse.
|
||||
// Buffers larger than 16KB are not pooled to prevent excessive memory retention.
|
||||
// The buffer is reset before being returned to the pool.
|
||||
func (m *MemoryPoolManager) PutCompressionBuffer(buf *bytes.Buffer) {
|
||||
if buf == nil {
|
||||
return
|
||||
|
||||
+5
-2
@@ -8,6 +8,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// MetadataCache provides thread-safe caching for OIDC provider metadata.
|
||||
// It stores provider discovery information (endpoints, issuer, etc.) to reduce
|
||||
// network requests to the provider's .well-known/openid-configuration endpoint.
|
||||
// The cache includes automatic expiration and periodic cleanup.
|
||||
type MetadataCache struct {
|
||||
expiresAt time.Time
|
||||
metadata *ProviderMetadata
|
||||
@@ -50,8 +54,7 @@ func (c *MetadataCache) Cleanup() {
|
||||
}
|
||||
|
||||
// isCacheValid checks if the cached metadata is present and has not expired.
|
||||
// Note: This function assumes the read lock is held or it's called from a context
|
||||
// where the lock is already held (like within GetMetadata after locking).
|
||||
// This method assumes the caller holds the appropriate lock.
|
||||
func (c *MetadataCache) isCacheValid() bool {
|
||||
return c.metadata != nil && time.Now().Before(c.expiresAt)
|
||||
}
|
||||
|
||||
+16
-6
@@ -9,7 +9,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// SecurityEventType represents different types of security events
|
||||
// SecurityEventType represents different categories of security events
|
||||
// that can occur during OIDC authentication and authorization flows.
|
||||
type SecurityEventType string
|
||||
|
||||
const (
|
||||
@@ -23,7 +24,8 @@ const (
|
||||
SuspiciousActivity SecurityEventType = "suspicious_activity"
|
||||
)
|
||||
|
||||
// DefaultSeverity returns the default severity level for a security event type
|
||||
// DefaultSeverity returns the default severity level for a security event type.
|
||||
// Severity levels are: low, medium, high.
|
||||
func (t SecurityEventType) DefaultSeverity() string {
|
||||
switch t {
|
||||
case AuthFailure:
|
||||
@@ -39,7 +41,9 @@ func (t SecurityEventType) DefaultSeverity() string {
|
||||
}
|
||||
}
|
||||
|
||||
// IPFailureType returns the IP failure tracking type for a security event type
|
||||
// IPFailureType returns the appropriate IP failure tracking category
|
||||
// for a given security event type. This is used to categorize failures
|
||||
// by IP address for rate limiting and blocking decisions.
|
||||
func (t SecurityEventType) IPFailureType() string {
|
||||
switch t {
|
||||
case AuthFailure:
|
||||
@@ -53,7 +57,9 @@ func (t SecurityEventType) IPFailureType() string {
|
||||
}
|
||||
}
|
||||
|
||||
// SecurityEvent represents a security-related event that should be logged and monitored
|
||||
// SecurityEvent represents a security-related event that should be logged and monitored.
|
||||
// It captures comprehensive context about the event including timestamp, client information,
|
||||
// request details, and custom event-specific data.
|
||||
type SecurityEvent struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
@@ -65,7 +71,9 @@ type SecurityEvent struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SecurityMonitor tracks security events and suspicious activity patterns
|
||||
// SecurityMonitor provides centralized security event tracking and analysis.
|
||||
// It monitors authentication failures, detects suspicious patterns, enforces
|
||||
// rate limits, and can trigger custom security event handlers.
|
||||
type SecurityMonitor struct {
|
||||
ipFailures map[string]*IPFailureTracker
|
||||
patternDetector *SuspiciousPatternDetector
|
||||
@@ -75,7 +83,9 @@ type SecurityMonitor struct {
|
||||
ipMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// IPFailureTracker tracks failures for a specific IP address
|
||||
// IPFailureTracker maintains failure statistics for a specific IP address.
|
||||
// It tracks different types of failures, timestamps, and counts to support
|
||||
// rate limiting and IP blocking decisions.
|
||||
type IPFailureTracker struct {
|
||||
LastFailure time.Time
|
||||
FirstFailure time.Time
|
||||
|
||||
+40
-13
@@ -43,7 +43,6 @@ func generateSecureRandomString(length int) (string, error) {
|
||||
|
||||
// Cookie names and configuration constants used for session management
|
||||
const (
|
||||
// Using fixed prefixes for consistent cookie naming across restarts
|
||||
mainCookieName = "_oidc_raczylo_m"
|
||||
accessTokenCookie = "_oidc_raczylo_a"
|
||||
refreshTokenCookie = "_oidc_raczylo_r"
|
||||
@@ -67,8 +66,8 @@ const (
|
||||
minEncryptionKeyLength = 32
|
||||
)
|
||||
|
||||
// compressToken compresses the input string using gzip and then encodes the result using standard base64 encoding.
|
||||
// Enhanced compression with robust integrity verification and JWT format validation.
|
||||
// compressToken compresses JWT tokens using gzip and encodes the result with base64.
|
||||
// It validates JWT format before compression and includes size limits for safety.
|
||||
//
|
||||
// Parameters:
|
||||
// - token: The string to compress.
|
||||
@@ -80,8 +79,7 @@ func compressToken(token string) string {
|
||||
return token
|
||||
}
|
||||
|
||||
// FIXED: For test compatibility, invalid tokens should be returned unchanged
|
||||
// Only compress valid JWTs (exactly 2 dots)
|
||||
// Only compress valid JWT tokens (must have exactly 2 dots)
|
||||
dotCount := strings.Count(token, ".")
|
||||
if dotCount != 2 {
|
||||
return token
|
||||
@@ -92,7 +90,7 @@ func compressToken(token string) string {
|
||||
return token
|
||||
}
|
||||
|
||||
// ENHANCED: Use memory pool for compression buffer
|
||||
// Use memory pool for efficient buffer management
|
||||
pools := GetGlobalMemoryPools()
|
||||
b := pools.GetCompressionBuffer()
|
||||
defer pools.PutCompressionBuffer(b)
|
||||
@@ -177,7 +175,7 @@ func decompressTokenInternal(compressed string) string {
|
||||
return compressed
|
||||
}
|
||||
|
||||
// ENHANCED: Use memory pool for decompression buffer
|
||||
// Use memory pool for efficient buffer management
|
||||
pools := GetGlobalMemoryPools()
|
||||
readerBuf := pools.GetHTTPResponseBuffer()
|
||||
defer pools.PutHTTPResponseBuffer(readerBuf)
|
||||
@@ -235,6 +233,11 @@ func decompressTokenInternal(compressed string) string {
|
||||
// SessionManager handles the management of multiple session cookies for OIDC authentication.
|
||||
// It provides functionality for storing and retrieving authentication state, tokens,
|
||||
// and other session-related data across multiple cookies.
|
||||
// SessionManager manages OIDC session data for the middleware.
|
||||
// It handles session creation, retrieval, and storage using encrypted cookies.
|
||||
// Large tokens are automatically chunked across multiple cookies to handle
|
||||
// browser size limitations. The manager uses a sync.Pool for efficient
|
||||
// session object reuse and supports both HTTP and HTTPS schemes.
|
||||
type SessionManager struct {
|
||||
sessionPool sync.Pool
|
||||
store sessions.Store
|
||||
@@ -281,6 +284,10 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*
|
||||
return sm, nil
|
||||
}
|
||||
|
||||
// PeriodicChunkCleanup performs periodic maintenance on session chunks.
|
||||
// It identifies and removes orphaned chunks that are no longer associated
|
||||
// with active sessions. This method should be called periodically to
|
||||
// prevent cookie accumulation in client browsers.
|
||||
func (sm *SessionManager) PeriodicChunkCleanup() {
|
||||
sm.logger.Debug("Starting comprehensive session cleanup cycle")
|
||||
|
||||
@@ -322,6 +329,17 @@ func (sm *SessionManager) PeriodicChunkCleanup() {
|
||||
//
|
||||
// Returns:
|
||||
// - error: nil if session is healthy, otherwise an error describing the issue
|
||||
//
|
||||
// ValidateSessionHealth performs comprehensive health checks on a session.
|
||||
// It validates authentication state, token formats, and checks for signs
|
||||
// of session tampering or corruption.
|
||||
//
|
||||
// Parameters:
|
||||
// - sessionData: The session to validate.
|
||||
//
|
||||
// Returns:
|
||||
// - nil if the session is healthy.
|
||||
// - An error describing any validation failures.
|
||||
func (sm *SessionManager) ValidateSessionHealth(sessionData *SessionData) error {
|
||||
if sessionData == nil {
|
||||
return fmt.Errorf("session data is nil")
|
||||
@@ -366,7 +384,16 @@ func (sm *SessionManager) ValidateSessionHealth(sessionData *SessionData) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTokenFormat performs basic format validation on tokens
|
||||
// validateTokenFormat performs basic structural validation on tokens.
|
||||
// It checks for proper JWT format and validates against maximum size limits.
|
||||
//
|
||||
// Parameters:
|
||||
// - token: The token string to validate.
|
||||
// - tokenType: The type of token (for error messages).
|
||||
//
|
||||
// Returns:
|
||||
// - nil if the token format is valid.
|
||||
// - An error if the token has invalid structure or exceeds size limits.
|
||||
func (sm *SessionManager) validateTokenFormat(token, tokenType string) error {
|
||||
if token == "" {
|
||||
return nil // Empty tokens are allowed
|
||||
@@ -436,7 +463,7 @@ func (sm *SessionManager) GetSessionMetrics() map[string]interface{} {
|
||||
metrics["has_encryption"] = false
|
||||
}
|
||||
|
||||
// Note: We can't easily get pool stats from sync.Pool as it doesn't expose them
|
||||
// sync.Pool doesn't expose internal statistics
|
||||
metrics["pool_implementation"] = "sync.Pool"
|
||||
|
||||
return metrics
|
||||
@@ -456,7 +483,7 @@ func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *h
|
||||
options = &sessions.Options{}
|
||||
}
|
||||
|
||||
// Enhanced security based on request analysis
|
||||
// Apply security settings based on request context
|
||||
if r != nil {
|
||||
// Check for suspicious request patterns
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
@@ -467,7 +494,7 @@ func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *h
|
||||
options.MaxAge = int((absoluteSessionTimeout / 2).Seconds())
|
||||
}
|
||||
|
||||
// Enhanced security for production environments
|
||||
// Apply stricter security settings for production
|
||||
if r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil || sm.forceHTTPS {
|
||||
options.Secure = true
|
||||
// Enable strict same-site policy for secure connections
|
||||
@@ -825,7 +852,7 @@ func (sd *SessionData) clearAllSessionData(r *http.Request, expire bool) {
|
||||
// Returns:
|
||||
// - An error if saving the expired sessions fails (only if w is not nil).
|
||||
//
|
||||
// Note: This method will always return the SessionData object to the pool, even if an error occurs.
|
||||
// This method ensures the SessionData object is always returned to the pool.
|
||||
func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
||||
// Use defer to guarantee session is returned to pool regardless of any errors or panics
|
||||
defer func() {
|
||||
@@ -1125,7 +1152,7 @@ func (sd *SessionData) SetAccessToken(token string) {
|
||||
// Compress token with validation
|
||||
compressed := compressToken(token)
|
||||
|
||||
// FIXED: Size validation after compression
|
||||
// Validate size after compression
|
||||
if len(compressed) > 100*1024 { // 100KB limit for final token (post-compression)
|
||||
sd.manager.logger.Info("Access token too large after compression (%d bytes) - storing uncompressed", len(compressed))
|
||||
return
|
||||
|
||||
+57
-12
@@ -11,7 +11,9 @@ import (
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
// TokenConfig holds validation rules for different token types
|
||||
// TokenConfig defines validation rules and constraints for different token types.
|
||||
// It specifies size limits, chunking parameters, and format requirements to ensure
|
||||
// tokens can be safely stored in browser cookies while maintaining security.
|
||||
type TokenConfig struct {
|
||||
Type string
|
||||
MinLength int
|
||||
@@ -55,19 +57,31 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// TokenRetrievalResult encapsulates the result of token retrieval
|
||||
// TokenRetrievalResult encapsulates the result of a token retrieval operation.
|
||||
// It contains either a successfully retrieved token or an error describing
|
||||
// what went wrong during retrieval.
|
||||
type TokenRetrievalResult struct {
|
||||
Token string
|
||||
Error error
|
||||
}
|
||||
|
||||
// ChunkManager handles token chunking operations
|
||||
// ChunkManager provides thread-safe operations for splitting large tokens
|
||||
// into smaller chunks that fit within browser cookie size limits. It handles
|
||||
// the chunking and reassembly of tokens transparently, ensuring data integrity
|
||||
// throughout the process.
|
||||
type ChunkManager struct {
|
||||
logger *Logger
|
||||
mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// NewChunkManager creates a new ChunkManager instance
|
||||
// NewChunkManager creates a new ChunkManager instance with the specified logger.
|
||||
// If no logger is provided, a no-op logger is used to prevent nil pointer errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - logger: The logger instance for recording chunk operations.
|
||||
//
|
||||
// Returns:
|
||||
// - A new ChunkManager instance ready for use.
|
||||
func NewChunkManager(logger *Logger) *ChunkManager {
|
||||
if logger == nil {
|
||||
logger = newNoOpLogger()
|
||||
@@ -80,6 +94,18 @@ func NewChunkManager(logger *Logger) *ChunkManager {
|
||||
}
|
||||
|
||||
// GetToken retrieves and validates a token from either single storage or chunks
|
||||
// GetToken retrieves and validates a token, handling both single-cookie
|
||||
// and chunked storage scenarios. It performs decompression if needed and
|
||||
// validates the token according to the provided configuration.
|
||||
//
|
||||
// Parameters:
|
||||
// - singleToken: The token string if stored in a single cookie.
|
||||
// - compressed: Whether the token is compressed.
|
||||
// - chunks: Map of session chunks if token is split across cookies.
|
||||
// - config: Token validation configuration.
|
||||
//
|
||||
// Returns:
|
||||
// - TokenRetrievalResult containing the token or an error.
|
||||
func (cm *ChunkManager) GetToken(
|
||||
singleToken string,
|
||||
compressed bool,
|
||||
@@ -102,7 +128,17 @@ func (cm *ChunkManager) GetToken(
|
||||
return cm.processChunkedToken(chunks, config)
|
||||
}
|
||||
|
||||
// processSingleToken handles tokens stored in a single cookie
|
||||
// processSingleToken processes tokens stored in a single cookie.
|
||||
// It handles decompression if needed and performs comprehensive validation
|
||||
// including corruption detection, format validation, and size checks.
|
||||
//
|
||||
// Parameters:
|
||||
// - token: The token string from the cookie.
|
||||
// - compressed: Whether the token needs decompression.
|
||||
// - config: Token validation configuration.
|
||||
//
|
||||
// Returns:
|
||||
// - TokenRetrievalResult containing the processed token or an error.
|
||||
func (cm *ChunkManager) processSingleToken(token string, compressed bool, config TokenConfig) TokenRetrievalResult {
|
||||
// Detect corruption markers
|
||||
if isCorruptionMarker(token) {
|
||||
@@ -130,19 +166,28 @@ func (cm *ChunkManager) processSingleToken(token string, compressed bool, config
|
||||
return cm.validateToken(finalToken, config)
|
||||
}
|
||||
|
||||
// validateToken performs comprehensive token validation
|
||||
// validateToken performs comprehensive validation on a token.
|
||||
// It checks size limits, chunking efficiency, content validity,
|
||||
// expiration, freshness, and format requirements based on the token configuration.
|
||||
//
|
||||
// Parameters:
|
||||
// - token: The token string to validate.
|
||||
// - config: Token validation configuration.
|
||||
//
|
||||
// Returns:
|
||||
// - TokenRetrievalResult with the validated token or validation error.
|
||||
func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRetrievalResult {
|
||||
// Enhanced size validation
|
||||
// Validate token size against configured limits
|
||||
if sizeErr := cm.validateTokenSize(token, config); sizeErr != nil {
|
||||
return TokenRetrievalResult{Token: "", Error: sizeErr}
|
||||
}
|
||||
|
||||
// Chunking efficiency validation (for pre-storage analysis)
|
||||
// Check if token would chunk efficiently
|
||||
if chunkErr := cm.validateChunkingEfficiency(token, config); chunkErr != nil {
|
||||
return TokenRetrievalResult{Token: "", Error: chunkErr}
|
||||
}
|
||||
|
||||
// Comprehensive content validation
|
||||
// Validate token content and structure
|
||||
if contentErr := cm.validateTokenContent(token, config); contentErr != nil {
|
||||
return TokenRetrievalResult{Token: "", Error: contentErr}
|
||||
}
|
||||
@@ -157,7 +202,7 @@ func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRet
|
||||
return TokenRetrievalResult{Token: "", Error: freshnessErr}
|
||||
}
|
||||
|
||||
// Enhanced JWT format validation
|
||||
// Validate JWT format if required
|
||||
if config.RequireJWTFormat && !config.AllowOpaqueTokens {
|
||||
if validationErr := cm.validateJWTFormat(token, config.Type); validationErr != nil {
|
||||
return TokenRetrievalResult{Token: "", Error: validationErr}
|
||||
@@ -182,7 +227,7 @@ func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRet
|
||||
|
||||
// processChunkedToken handles tokens stored across multiple chunks
|
||||
func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, config TokenConfig) TokenRetrievalResult {
|
||||
// Enhanced chunk count validation using config limits
|
||||
// Validate chunk count against configured maximum
|
||||
if len(chunks) > config.MaxChunks {
|
||||
err := fmt.Errorf("too many %s token chunks (%d, max: %d)", config.Type, len(chunks), config.MaxChunks)
|
||||
cm.logger.Info("Token chunk count exceeded for %s: %d chunks", config.Type, len(chunks))
|
||||
@@ -222,7 +267,7 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
// Enhanced chunk size validation using config limits
|
||||
// Validate individual chunk sizes
|
||||
if len(chunk) > config.MaxChunkSize {
|
||||
err := fmt.Errorf("%s token chunk %d exceeds size limit (%d bytes, max: %d)",
|
||||
config.Type, i, len(chunk), config.MaxChunkSize)
|
||||
|
||||
+10
-10
@@ -181,7 +181,7 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("refreshGracePeriodSeconds cannot be negative")
|
||||
}
|
||||
|
||||
// SECURITY FIX: Validate headers configuration with enhanced template security
|
||||
// Validate headers configuration for template security
|
||||
for _, header := range c.Headers {
|
||||
if header.Name == "" {
|
||||
return fmt.Errorf("header name cannot be empty")
|
||||
@@ -207,7 +207,7 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("header template '%s' appears to use lowercase 'refreshToken' - use '{{.RefreshToken...' instead (case sensitive)", header.Value)
|
||||
}
|
||||
|
||||
// SECURITY FIX: Implement template sandboxing and validation
|
||||
// Validate template syntax and security
|
||||
if err := validateTemplateSecure(header.Value); err != nil {
|
||||
return fmt.Errorf("header template '%s' failed security validation: %w", header.Value, err)
|
||||
}
|
||||
@@ -216,9 +216,9 @@ func (c *Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SECURITY FIX: validateTemplateSecure implements template sandboxing and validation
|
||||
// validateTemplateSecure validates template expressions for security vulnerabilities
|
||||
func validateTemplateSecure(templateStr string) error {
|
||||
// SECURITY FIX: Restrict dangerous template functions and patterns
|
||||
// Check for dangerous template functions and patterns
|
||||
dangerousPatterns := []string{
|
||||
"{{call", // Function calls
|
||||
"{{range", // Range over arbitrary data
|
||||
@@ -256,7 +256,7 @@ func validateTemplateSecure(templateStr string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// SECURITY FIX: Whitelist allowed template variables and functions
|
||||
// Validate template variables against whitelist
|
||||
allowedPatterns := []string{
|
||||
"{{.AccessToken}}",
|
||||
"{{.IdToken}}",
|
||||
@@ -277,7 +277,7 @@ func validateTemplateSecure(templateStr string) error {
|
||||
return fmt.Errorf("template must use only allowed variables: AccessToken, IdToken, RefreshToken, or Claims.*")
|
||||
}
|
||||
|
||||
// SECURITY FIX: Validate Claims access patterns
|
||||
// Validate claims access patterns
|
||||
if strings.Contains(templateStr, "{{.Claims.") {
|
||||
// Simple validation - ensure claims access is to known safe fields
|
||||
safeClaimsFields := map[string]bool{
|
||||
@@ -314,7 +314,7 @@ func validateTemplateSecure(templateStr string) error {
|
||||
return fmt.Errorf("access to Claims.%s is not allowed for security reasons", fieldName)
|
||||
}
|
||||
|
||||
// Fix the search for next occurrence
|
||||
// Search for next occurrence
|
||||
nextStart := strings.Index(templateStr[start+end+2:], "{{.Claims.")
|
||||
if nextStart != -1 {
|
||||
start = start + end + 2 + nextStart
|
||||
@@ -324,7 +324,7 @@ func validateTemplateSecure(templateStr string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// SECURITY FIX: Prevent code injection through template syntax
|
||||
// Prevent code injection through template syntax
|
||||
if strings.Contains(templateStr, "{{") && strings.Contains(templateStr, "}}") {
|
||||
// Count opening and closing braces
|
||||
openCount := strings.Count(templateStr, "{{")
|
||||
@@ -418,7 +418,7 @@ func (l *Logger) Info(format string, args ...interface{}) {
|
||||
l.logInfo.Printf(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs a message at the DEBUG level using Printf style formatting.
|
||||
// Debug logs a message at the DEBUG level.
|
||||
// Output is directed to stdout only if the configured log level is "debug".
|
||||
//
|
||||
// Parameters:
|
||||
@@ -449,7 +449,7 @@ func (l *Logger) Infof(format string, args ...interface{}) {
|
||||
l.logInfo.Printf(format, args...)
|
||||
}
|
||||
|
||||
// Debugf logs a message at the DEBUG level using Printf style formatting.
|
||||
// Debugf logs a formatted message at the DEBUG level.
|
||||
// Equivalent to calling l.Debug(format, args...).
|
||||
// Output is directed to stdout only if the configured log level is "debug".
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user