fixup! fixup! fixup! fixup! fixup! Cleanup tests.

This commit is contained in:
2025-07-24 10:29:08 +01:00
parent 00ed2ea61b
commit a4b2bfd70f
14 changed files with 406 additions and 133 deletions
+20 -5
View File
@@ -2,7 +2,9 @@ package traefikoidc
import "time" 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 { type BackgroundTask struct {
stopChan chan struct{} stopChan chan struct{}
taskFunc func() taskFunc func()
@@ -11,7 +13,16 @@ type BackgroundTask struct {
interval time.Duration 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 { func NewBackgroundTask(name string, interval time.Duration, taskFunc func(), logger *Logger) *BackgroundTask {
return &BackgroundTask{ return &BackgroundTask{
name: name, 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() { func (bt *BackgroundTask) Start() {
go bt.run() 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() { func (bt *BackgroundTask) Stop() {
close(bt.stopChan) 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() { func (bt *BackgroundTask) run() {
ticker := time.NewTicker(bt.interval) ticker := time.NewTicker(bt.interval)
defer ticker.Stop() defer ticker.Stop()
+27 -7
View File
@@ -6,10 +6,12 @@ import (
) )
// MaxKeyLength defines the maximum allowed length for cache keys // MaxKeyLength defines the maximum allowed length for cache keys
// to prevent memory exhaustion from excessively long keys.
const MaxKeyLength = 256 const MaxKeyLength = 256
// OptimizedCacheEntry represents a single cache entry with embedded LRU linked list // OptimizedCacheEntry represents a single cache entry with embedded LRU linked list pointers.
// This eliminates the need for separate data structures and reduces memory overhead by ~66% // 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 { type OptimizedCacheEntry struct {
Value interface{} Value interface{}
ExpiresAt time.Time ExpiresAt time.Time
@@ -19,8 +21,10 @@ type OptimizedCacheEntry struct {
prev, next *OptimizedCacheEntry prev, next *OptimizedCacheEntry
} }
// OptimizedCache provides a memory-efficient thread-safe cache with LRU eviction // OptimizedCache provides a memory-efficient, thread-safe cache with LRU eviction policy.
// Uses only a single map with embedded doubly-linked list to reduce memory overhead // 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 { type OptimizedCache struct {
items map[string]*OptimizedCacheEntry items map[string]*OptimizedCacheEntry
head, tail *OptimizedCacheEntry // LRU sentinel nodes head, tail *OptimizedCacheEntry // LRU sentinel nodes
@@ -33,12 +37,21 @@ type OptimizedCache struct {
mutex sync.RWMutex 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 { func NewOptimizedCache() *OptimizedCache {
return NewOptimizedCacheWithConfig(DefaultMaxSize, 0, nil) 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 { func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *OptimizedCache {
if logger == nil { if logger == nil {
logger = newNoOpLogger() logger = newNoOpLogger()
@@ -69,7 +82,14 @@ func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *
return c 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) { func (c *OptimizedCache) Set(key string, value interface{}, expiration time.Duration) {
// Validate key length to prevent memory bloat // Validate key length to prevent memory bloat
if len(key) > MaxKeyLength { if len(key) > MaxKeyLength {
+22 -7
View File
@@ -11,7 +11,9 @@ import (
"time" "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 { type ErrorRecoveryMechanism interface {
// ExecuteWithContext executes a function with error recovery // ExecuteWithContext executes a function with error recovery
ExecuteWithContext(ctx context.Context, fn func() error) error ExecuteWithContext(ctx context.Context, fn func() error) error
@@ -23,7 +25,9 @@ type ErrorRecoveryMechanism interface {
IsAvailable() bool 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 { type BaseRecoveryMechanism struct {
startTime time.Time startTime time.Time
lastFailureTime time.Time lastFailureTime time.Time
@@ -36,7 +40,14 @@ type BaseRecoveryMechanism struct {
mutex sync.RWMutex 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 { func NewBaseRecoveryMechanism(name string, logger *Logger) *BaseRecoveryMechanism {
if logger == nil { if logger == nil {
logger = newNoOpLogger() 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() { func (b *BaseRecoveryMechanism) RecordRequest() {
atomic.AddInt64(&b.totalRequests, 1) 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() { func (b *BaseRecoveryMechanism) RecordSuccess() {
atomic.AddInt64(&b.totalSuccesses, 1) atomic.AddInt64(&b.totalSuccesses, 1)
@@ -63,7 +76,8 @@ func (b *BaseRecoveryMechanism) RecordSuccess() {
b.lastSuccessTime = time.Now() 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() { func (b *BaseRecoveryMechanism) RecordFailure() {
atomic.AddInt64(&b.totalFailures, 1) atomic.AddInt64(&b.totalFailures, 1)
@@ -72,7 +86,8 @@ func (b *BaseRecoveryMechanism) RecordFailure() {
b.lastFailureTime = time.Now() 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{} { func (b *BaseRecoveryMechanism) GetBaseMetrics() map[string]interface{} {
b.mutex.RLock() b.mutex.RLock()
defer b.mutex.RUnlock() defer b.mutex.RUnlock()
+3
View File
@@ -209,6 +209,9 @@ func extractClaims(tokenString string) (map[string]interface{}, error) {
// TokenCache provides a caching mechanism for validated tokens. // TokenCache provides a caching mechanism for validated tokens.
// It stores token claims to avoid repeated validation of the // It stores token claims to avoid repeated validation of the
// same token, improving performance for frequently used tokens. // 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 { type TokenCache struct {
cache *Cache cache *Cache
} }
+22 -4
View File
@@ -10,6 +10,9 @@ import (
) )
// InputValidator provides comprehensive input validation and sanitization // 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 { type InputValidator struct {
usernameRegex *regexp.Regexp usernameRegex *regexp.Regexp
tokenRegex *regexp.Regexp tokenRegex *regexp.Regexp
@@ -27,7 +30,9 @@ type InputValidator struct {
maxHeaderLength int 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 { type ValidationResult struct {
SanitizedValue string `json:"sanitized_value,omitempty"` SanitizedValue string `json:"sanitized_value,omitempty"`
SecurityRisk string `json:"security_risk,omitempty"` SecurityRisk string `json:"security_risk,omitempty"`
@@ -36,7 +41,9 @@ type ValidationResult struct {
IsValid bool `json:"is_valid"` 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 { type InputValidationConfig struct {
MaxTokenLength int `json:"max_token_length"` MaxTokenLength int `json:"max_token_length"`
MaxURLLength int `json:"max_url_length"` MaxURLLength int `json:"max_url_length"`
@@ -47,7 +54,9 @@ type InputValidationConfig struct {
StrictMode bool `json:"strict_mode"` 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 { func DefaultInputValidationConfig() InputValidationConfig {
return InputValidationConfig{ return InputValidationConfig{
MaxTokenLength: 50000, // 50KB for tokens 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) { func NewInputValidator(config InputValidationConfig, logger *Logger) (*InputValidator, error) {
// Compile regex patterns // Compile regex patterns
emailRegex, err := regexp.Compile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) emailRegex, err := regexp.Compile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
+26 -10
View File
@@ -16,22 +16,32 @@ import (
"time" "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 { type JWK struct {
Kty string `json:"kty"` Kty string `json:"kty"` // Key type (RSA, EC)
Kid string `json:"kid"` Kid string `json:"kid"` // Key ID
Use string `json:"use"` Use string `json:"use"` // Key use (sig, enc)
N string `json:"n"` N string `json:"n"` // RSA modulus
E string `json:"e"` E string `json:"e"` // RSA public exponent
Alg string `json:"alg"` Alg string `json:"alg"` // Algorithm
Crv string `json:"crv"` Crv string `json:"crv"` // ECDSA curve
X string `json:"x"` X string `json:"x"` // ECDSA x coordinate
Y string `json:"y"` 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 { type JWKSet struct {
Keys []JWK `json:"keys"` 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 { type JWKCache struct {
expiresAt time.Time expiresAt time.Time
jwks *JWKSet jwks *JWKSet
@@ -41,6 +51,9 @@ type JWKCache struct {
mutex sync.RWMutex 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 { type JWKCacheInterface interface {
GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error) GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error)
Cleanup() 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. // Cleanup removes the cached JWKS if it has expired.
// This is intended to be called periodically to ensure stale JWKS data is cleared. // 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() { func (c *JWKCache) Cleanup() {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
@@ -142,7 +158,7 @@ func (c *JWKCache) Cleanup() {
// Close shuts down the cache's auto-cleanup routine. // Close shuts down the cache's auto-cleanup routine.
func (c *JWKCache) Close() { 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 { if c.internalCache != nil {
c.internalCache.Close() c.internalCache.Close()
} }
+24 -1
View File
@@ -22,6 +22,9 @@ var (
replayCacheOnce sync.Once 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() { func initReplayCache() {
replayCacheOnce.Do(func() { replayCacheOnce.Do(func() {
replayCache = NewCache() 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() { func cleanupReplayCache() {
replayCacheMu.Lock() replayCacheMu.Lock()
defer replayCacheMu.Unlock() 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) { func getReplayCacheStats() (size int, maxSize int) {
replayCacheMu.RLock() replayCacheMu.RLock()
defer replayCacheMu.RUnlock() defer replayCacheMu.RUnlock()
@@ -50,6 +63,13 @@ func getReplayCacheStats() (size int, maxSize int) {
return 0, 10000 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) { func startReplayCacheCleanup(ctx context.Context, logger *Logger) {
go func() { go func() {
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(5 * time.Minute)
@@ -87,6 +107,9 @@ var ClockSkewTolerancePast = 10 * time.Second
var ClockSkewTolerance = ClockSkewToleranceFuture var ClockSkewTolerance = ClockSkewToleranceFuture
// JWT represents a JSON Web Token as defined in RFC 7519. // 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 { type JWT struct {
Header map[string]interface{} Header map[string]interface{}
Claims 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)) 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() pools := GetGlobalMemoryPools()
jwtBuf := pools.GetJWTParsingBuffer() jwtBuf := pools.GetJWTParsingBuffer()
defer pools.PutJWTParsingBuffer(jwtBuf) defer pools.PutJWTParsingBuffer(jwtBuf)
+120 -52
View File
@@ -36,30 +36,30 @@ func createDefaultHTTPClient() *http.Client {
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := &net.Dialer{ dialer := &net.Dialer{
Timeout: 10 * time.Second, // OPTIMIZED: Further reduced for faster failures Timeout: 10 * time.Second, // Connection timeout for faster failure detection
KeepAlive: 30 * time.Second, // OPTIMIZED: Increased for better connection reuse KeepAlive: 30 * time.Second, // Keep-alive interval for connection reuse
} }
return dialer.DialContext(ctx, network, addr) return dialer.DialContext(ctx, network, addr)
}, },
ForceAttemptHTTP2: true, ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 3 * time.Second, // OPTIMIZED: Reduced for faster TLS negotiation TLSHandshakeTimeout: 3 * time.Second, // TLS handshake timeout
ExpectContinueTimeout: 1 * time.Second, // OPTIMIZED: Enable for better upload performance ExpectContinueTimeout: 1 * time.Second, // Timeout for 100-continue responses
MaxIdleConns: 20, // OPTIMIZED: Reduced to limit memory usage MaxIdleConns: 20, // Maximum idle connections across all hosts
MaxIdleConnsPerHost: 5, // OPTIMIZED: Reduced per-host connections MaxIdleConnsPerHost: 5, // Maximum idle connections per host
IdleConnTimeout: 60 * time.Second, // OPTIMIZED: Increased for better reuse IdleConnTimeout: 60 * time.Second, // Maximum idle time before connection close
DisableKeepAlives: false, // Enable connection reuse DisableKeepAlives: false, // Enable connection reuse
MaxConnsPerHost: 20, // OPTIMIZED: Reduced to limit memory usage MaxConnsPerHost: 20, // Maximum connections per host
ResponseHeaderTimeout: 5 * time.Second, // OPTIMIZED: Added response header timeout ResponseHeaderTimeout: 5 * time.Second, // Timeout for reading response headers
DisableCompression: false, // Enable compression for bandwidth efficiency DisableCompression: false, // Enable compression for bandwidth efficiency
WriteBufferSize: 4096, // OPTIMIZED: Set optimal buffer size WriteBufferSize: 4096, // Write buffer size for connections
ReadBufferSize: 4096, // OPTIMIZED: Set optimal buffer size ReadBufferSize: 4096, // Read buffer size for connections
} }
return &http.Client{ return &http.Client{
Timeout: time.Second * 10, // OPTIMIZED: Reduced timeout for faster failures Timeout: time.Second * 10, // HTTP client timeout
Transport: transport, Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error { 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 { if len(via) >= 10 {
return fmt.Errorf("stopped after 10 redirects") return fmt.Errorf("stopped after 10 redirects")
} }
@@ -107,6 +107,10 @@ var (
) )
// CacheManager provides shared cache instances across middleware instances // 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 { type CacheManager struct {
tokenBlacklist *Cache tokenBlacklist *Cache
tokenCache *TokenCache tokenCache *TokenCache
@@ -115,7 +119,10 @@ type CacheManager struct {
mu sync.RWMutex 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 { func GetGlobalCacheManager() *CacheManager {
cacheManagerOnce.Do(func() { cacheManagerOnce.Do(func() {
globalCacheManager = &CacheManager{ globalCacheManager = &CacheManager{
@@ -132,35 +139,45 @@ func GetGlobalCacheManager() *CacheManager {
return globalCacheManager 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 { func (cm *CacheManager) GetSharedTokenBlacklist() *Cache {
cm.mu.RLock() cm.mu.RLock()
defer cm.mu.RUnlock() defer cm.mu.RUnlock()
return cm.tokenBlacklist 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 { func (cm *CacheManager) GetSharedTokenCache() *TokenCache {
cm.mu.RLock() cm.mu.RLock()
defer cm.mu.RUnlock() defer cm.mu.RUnlock()
return cm.tokenCache 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 { func (cm *CacheManager) GetSharedMetadataCache() *MetadataCache {
cm.mu.RLock() cm.mu.RLock()
defer cm.mu.RUnlock() defer cm.mu.RUnlock()
return cm.metadataCache 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 { func (cm *CacheManager) GetSharedJWKCache() JWKCacheInterface {
cm.mu.RLock() cm.mu.RLock()
defer cm.mu.RUnlock() defer cm.mu.RUnlock()
return cm.jwkCache 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 { func (cm *CacheManager) Close() error {
cm.mu.Lock() cm.mu.Lock()
defer cm.mu.Unlock() defer cm.mu.Unlock()
@@ -181,24 +198,32 @@ func (cm *CacheManager) Close() error {
return nil 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 { type TokenVerifier interface {
VerifyToken(token string) error 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 { type JWTVerifier interface {
VerifyJWTSignatureAndClaims(jwt *JWT, token string) error 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 { type TokenExchanger interface {
ExchangeCodeForToken(ctx context.Context, grantType string, codeOrToken string, redirectURL string, codeVerifier string) (*TokenResponse, error) ExchangeCodeForToken(ctx context.Context, grantType string, codeOrToken string, redirectURL string, codeVerifier string) (*TokenResponse, error)
GetNewTokenWithRefreshToken(refreshToken string) (*TokenResponse, error) GetNewTokenWithRefreshToken(refreshToken string) (*TokenResponse, error)
RevokeTokenWithProvider(token, tokenType string) 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 { type TraefikOidc struct {
jwkCache JWKCacheInterface jwkCache JWKCacheInterface
jwtVerifier JWTVerifier jwtVerifier JWTVerifier
@@ -249,7 +274,10 @@ type TraefikOidc struct {
suppressDiagnosticLogs bool 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 { type ProviderMetadata struct {
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"` AuthURL string `json:"authorization_endpoint"`
@@ -314,9 +342,17 @@ var defaultExcludedURLs = map[string]struct{}{
// Parameters: // Parameters:
// - token: The raw ID token string to verify. // - 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: // Returns:
// - nil if the token is valid according to all checks. // - nil if the token passes all validation checks.
// - An error describing the reason for validation failure (e.g., rate limit, blacklisted, parsing error, signature error, claim error). // - 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 { func (t *TraefikOidc) VerifyToken(token string) error {
if token == "" { if token == "" {
return fmt.Errorf("invalid JWT format: token is empty") 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 jti, ok := parsedJWT.Claims["jti"].(string); ok && jti != "" {
if !strings.HasPrefix(token, "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0") { if !strings.HasPrefix(token, "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0") {
if blacklisted, exists := t.tokenBlacklist.Get(jti); exists && blacklisted != nil { 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 { if claims, exists := t.tokenCache.Get(token); exists && len(claims) > 0 {
// Token found in cache, skip signature verification // Token found in cache, skip signature verification
return nil return nil
@@ -941,10 +975,21 @@ func fetchMetadata(wellKnownURL string, httpClient *http.Client) (*ProviderMetad
return &metadata, nil return &metadata, nil
} }
// ServeHTTP is the main entry point for incoming requests to the middleware. // ServeHTTP implements the http.Handler interface and serves as the main entry point
// It orchestrates the OIDC authentication flow. // 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) { func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// --- Initialization Check --- // Wait for provider metadata initialization to complete
select { select {
case <-t.initComplete: case <-t.initComplete:
if t.issuerURL == "" { // Check if initialization actually succeeded if t.issuerURL == "" { // Check if initialization actually succeeded
@@ -962,7 +1007,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return return
} }
// --- Excluded Paths & SSE Check --- // Check if request should bypass authentication
if t.determineExcludedURL(req.URL.Path) { if t.determineExcludedURL(req.URL.Path) {
t.logger.Debugf("Request path %s excluded by configuration, bypassing OIDC", req.URL.Path) t.logger.Debugf("Request path %s excluded by configuration, bypassing OIDC", req.URL.Path)
t.next.ServeHTTP(rw, req) t.next.ServeHTTP(rw, req)
@@ -975,7 +1020,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return return
} }
// --- Session Retrieval --- // Retrieve or create session for request
session, err := t.sessionManager.GetSession(req) session, err := t.sessionManager.GetSession(req)
if err != nil { if err != nil {
// Log the specific session error // Log the specific session error
@@ -1377,7 +1422,6 @@ func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request,
csrfToken := session.GetCSRF() csrfToken := session.GetCSRF()
if csrfToken == "" { if csrfToken == "" {
// Enhanced logging for debugging
t.logger.Errorf("CSRF token missing in session during callback. Authenticated: %v, Request URL: %s", t.logger.Errorf("CSRF token missing in session during callback. Authenticated: %v, Request URL: %s",
session.GetAuthenticated(), req.URL.String()) session.GetAuthenticated(), req.URL.String())
@@ -1668,7 +1712,7 @@ func (t *TraefikOidc) defaultInitiateAuthentication(rw http.ResponseWriter, req
authURL := t.buildAuthURL(redirectURL, csrfToken, nonce, codeChallenge) authURL := t.buildAuthURL(redirectURL, csrfToken, nonce, codeChallenge)
t.logger.Debugf("Redirecting user to OIDC provider: %s", authURL) 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) http.Redirect(rw, req, authURL, http.StatusFound)
} }
@@ -1919,8 +1963,11 @@ func (t *TraefikOidc) validateHost(host string) error {
return nil return nil
} }
// startTokenCleanup starts background goroutines for periodically cleaning up // startTokenCleanup initiates a background goroutine that performs periodic
// the token cache, token blacklist cache, and JWK cache. // 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() { func (t *TraefikOidc) startTokenCleanup() {
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
t.goroutineWG.Add(1) // Track this goroutine t.goroutineWG.Add(1) // Track this goroutine
@@ -1929,7 +1976,7 @@ func (t *TraefikOidc) startTokenCleanup() {
t.goroutineWG.Done() // Signal completion when goroutine exits t.goroutineWG.Done() // Signal completion when goroutine exits
ticker.Stop() // Ensure ticker is always stopped 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 { if r := recover(); r != nil {
t.logger.Errorf("Token cleanup goroutine panic recovered: %v", r) 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. // Based on New(), t.jwkCache = &JWKCache{}, which has a Cleanup method.
t.jwkCache.Cleanup() t.jwkCache.Cleanup()
} }
// ENHANCED: Comprehensive session management with health monitoring // Perform comprehensive session cleanup and health monitoring
if t.sessionManager != nil { if t.sessionManager != nil {
t.sessionManager.PeriodicChunkCleanup() t.sessionManager.PeriodicChunkCleanup()
// Periodic session health monitoring // Periodic session health monitoring
t.logger.Debug("Running 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 // 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 // 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). // 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. // 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: // Parameters:
// - token: The raw token string to revoke locally. // - token: The raw token string to revoke locally.
func (t *TraefikOidc) RevokeToken(token string) { func (t *TraefikOidc) RevokeToken(token string) {
// SECURITY FIX: Ensure proper cache invalidation when tokens are blacklisted // Remove token from validation cache to ensure immediate invalidation
// Remove from cache
t.tokenCache.Delete(token) 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 jwt, err := parseJWT(token); err == nil {
if jti, ok := jwt.Claims["jti"].(string); ok && jti != "" { if jti, ok := jwt.Claims["jti"].(string); ok && jti != "" {
// Add JTI to blacklist as well // 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 // This allows the TraefikOidc struct to act as its own default TokenExchanger, while
// still allowing mocking for tests. // still allowing mocking for tests.
func (t *TraefikOidc) ExchangeCodeForToken(ctx context.Context, grantType string, codeOrToken string, redirectURL string, codeVerifier string) (*TokenResponse, error) { 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) 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 // This allows the TraefikOidc struct to act as its own default TokenExchanger, while
// still allowing mocking for tests. // still allowing mocking for tests.
func (t *TraefikOidc) GetNewTokenWithRefreshToken(refreshToken string) (*TokenResponse, error) { 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) 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 _, _ = 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 { func (t *TraefikOidc) isGoogleProvider() bool {
return strings.Contains(t.issuerURL, "google") || strings.Contains(t.issuerURL, "accounts.google.com") 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 { func (t *TraefikOidc) isAzureProvider() bool {
return strings.Contains(t.issuerURL, "login.microsoftonline.com") || return strings.Contains(t.issuerURL, "login.microsoftonline.com") ||
strings.Contains(t.issuerURL, "sts.windows.net") || strings.Contains(t.issuerURL, "sts.windows.net") ||
strings.Contains(t.issuerURL, "login.windows.net") strings.Contains(t.issuerURL, "login.windows.net")
} }
// validateAzureTokens handles Azure AD-specific token validation logic // validateAzureTokens performs Azure AD-specific token validation.
// Azure AD tokens may have different characteristics, so we validate access token first // 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) { func (t *TraefikOidc) validateAzureTokens(session *SessionData) (bool, bool, bool) {
if !session.GetAuthenticated() { if !session.GetAuthenticated() {
t.logger.Debug("Azure user is not authenticated according to session flag") 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 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 { func (t *TraefikOidc) Close() error {
var closeErr error var closeErr error
t.shutdownOnce.Do(func() { t.shutdownOnce.Do(func() {
+14 -4
View File
@@ -7,6 +7,9 @@ import (
) )
// MemoryPoolManager manages various memory pools for high-frequency allocations // 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 { type MemoryPoolManager struct {
compressionBufferPool *sync.Pool compressionBufferPool *sync.Pool
jwtParsingPool *sync.Pool jwtParsingPool *sync.Pool
@@ -14,14 +17,18 @@ type MemoryPoolManager struct {
stringBuilderPool *sync.Pool 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 { type JWTParsingBuffer struct {
HeaderBuf []byte HeaderBuf []byte
PayloadBuf []byte PayloadBuf []byte
SignatureBuf []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 { func NewMemoryPoolManager() *MemoryPoolManager {
return &MemoryPoolManager{ return &MemoryPoolManager{
// Pool for compression/decompression buffers (4KB default) // 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 { func (m *MemoryPoolManager) GetCompressionBuffer() *bytes.Buffer {
return m.compressionBufferPool.Get().(*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) { func (m *MemoryPoolManager) PutCompressionBuffer(buf *bytes.Buffer) {
if buf == nil { if buf == nil {
return return
+5 -2
View File
@@ -8,6 +8,10 @@ import (
"time" "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 { type MetadataCache struct {
expiresAt time.Time expiresAt time.Time
metadata *ProviderMetadata metadata *ProviderMetadata
@@ -50,8 +54,7 @@ func (c *MetadataCache) Cleanup() {
} }
// isCacheValid checks if the cached metadata is present and has not expired. // 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 // This method assumes the caller holds the appropriate lock.
// where the lock is already held (like within GetMetadata after locking).
func (c *MetadataCache) isCacheValid() bool { func (c *MetadataCache) isCacheValid() bool {
return c.metadata != nil && time.Now().Before(c.expiresAt) return c.metadata != nil && time.Now().Before(c.expiresAt)
} }
+16 -6
View File
@@ -9,7 +9,8 @@ import (
"time" "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 type SecurityEventType string
const ( const (
@@ -23,7 +24,8 @@ const (
SuspiciousActivity SecurityEventType = "suspicious_activity" 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 { func (t SecurityEventType) DefaultSeverity() string {
switch t { switch t {
case AuthFailure: 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 { func (t SecurityEventType) IPFailureType() string {
switch t { switch t {
case AuthFailure: 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 { type SecurityEvent struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Details map[string]interface{} `json:"details,omitempty"` Details map[string]interface{} `json:"details,omitempty"`
@@ -65,7 +71,9 @@ type SecurityEvent struct {
Message string `json:"message"` 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 { type SecurityMonitor struct {
ipFailures map[string]*IPFailureTracker ipFailures map[string]*IPFailureTracker
patternDetector *SuspiciousPatternDetector patternDetector *SuspiciousPatternDetector
@@ -75,7 +83,9 @@ type SecurityMonitor struct {
ipMutex sync.RWMutex 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 { type IPFailureTracker struct {
LastFailure time.Time LastFailure time.Time
FirstFailure time.Time FirstFailure time.Time
+40 -13
View File
@@ -43,7 +43,6 @@ func generateSecureRandomString(length int) (string, error) {
// Cookie names and configuration constants used for session management // Cookie names and configuration constants used for session management
const ( const (
// Using fixed prefixes for consistent cookie naming across restarts
mainCookieName = "_oidc_raczylo_m" mainCookieName = "_oidc_raczylo_m"
accessTokenCookie = "_oidc_raczylo_a" accessTokenCookie = "_oidc_raczylo_a"
refreshTokenCookie = "_oidc_raczylo_r" refreshTokenCookie = "_oidc_raczylo_r"
@@ -67,8 +66,8 @@ const (
minEncryptionKeyLength = 32 minEncryptionKeyLength = 32
) )
// compressToken compresses the input string using gzip and then encodes the result using standard base64 encoding. // compressToken compresses JWT tokens using gzip and encodes the result with base64.
// Enhanced compression with robust integrity verification and JWT format validation. // It validates JWT format before compression and includes size limits for safety.
// //
// Parameters: // Parameters:
// - token: The string to compress. // - token: The string to compress.
@@ -80,8 +79,7 @@ func compressToken(token string) string {
return token return token
} }
// FIXED: For test compatibility, invalid tokens should be returned unchanged // Only compress valid JWT tokens (must have exactly 2 dots)
// Only compress valid JWTs (exactly 2 dots)
dotCount := strings.Count(token, ".") dotCount := strings.Count(token, ".")
if dotCount != 2 { if dotCount != 2 {
return token return token
@@ -92,7 +90,7 @@ func compressToken(token string) string {
return token return token
} }
// ENHANCED: Use memory pool for compression buffer // Use memory pool for efficient buffer management
pools := GetGlobalMemoryPools() pools := GetGlobalMemoryPools()
b := pools.GetCompressionBuffer() b := pools.GetCompressionBuffer()
defer pools.PutCompressionBuffer(b) defer pools.PutCompressionBuffer(b)
@@ -177,7 +175,7 @@ func decompressTokenInternal(compressed string) string {
return compressed return compressed
} }
// ENHANCED: Use memory pool for decompression buffer // Use memory pool for efficient buffer management
pools := GetGlobalMemoryPools() pools := GetGlobalMemoryPools()
readerBuf := pools.GetHTTPResponseBuffer() readerBuf := pools.GetHTTPResponseBuffer()
defer pools.PutHTTPResponseBuffer(readerBuf) defer pools.PutHTTPResponseBuffer(readerBuf)
@@ -235,6 +233,11 @@ func decompressTokenInternal(compressed string) string {
// SessionManager handles the management of multiple session cookies for OIDC authentication. // SessionManager handles the management of multiple session cookies for OIDC authentication.
// It provides functionality for storing and retrieving authentication state, tokens, // It provides functionality for storing and retrieving authentication state, tokens,
// and other session-related data across multiple cookies. // 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 { type SessionManager struct {
sessionPool sync.Pool sessionPool sync.Pool
store sessions.Store store sessions.Store
@@ -281,6 +284,10 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*
return sm, nil 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() { func (sm *SessionManager) PeriodicChunkCleanup() {
sm.logger.Debug("Starting comprehensive session cleanup cycle") sm.logger.Debug("Starting comprehensive session cleanup cycle")
@@ -322,6 +329,17 @@ func (sm *SessionManager) PeriodicChunkCleanup() {
// //
// Returns: // Returns:
// - error: nil if session is healthy, otherwise an error describing the issue // - 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 { func (sm *SessionManager) ValidateSessionHealth(sessionData *SessionData) error {
if sessionData == nil { if sessionData == nil {
return fmt.Errorf("session data is nil") return fmt.Errorf("session data is nil")
@@ -366,7 +384,16 @@ func (sm *SessionManager) ValidateSessionHealth(sessionData *SessionData) error
return nil 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 { func (sm *SessionManager) validateTokenFormat(token, tokenType string) error {
if token == "" { if token == "" {
return nil // Empty tokens are allowed return nil // Empty tokens are allowed
@@ -436,7 +463,7 @@ func (sm *SessionManager) GetSessionMetrics() map[string]interface{} {
metrics["has_encryption"] = false 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" metrics["pool_implementation"] = "sync.Pool"
return metrics return metrics
@@ -456,7 +483,7 @@ func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *h
options = &sessions.Options{} options = &sessions.Options{}
} }
// Enhanced security based on request analysis // Apply security settings based on request context
if r != nil { if r != nil {
// Check for suspicious request patterns // Check for suspicious request patterns
userAgent := r.Header.Get("User-Agent") 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()) 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 { if r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil || sm.forceHTTPS {
options.Secure = true options.Secure = true
// Enable strict same-site policy for secure connections // Enable strict same-site policy for secure connections
@@ -825,7 +852,7 @@ func (sd *SessionData) clearAllSessionData(r *http.Request, expire bool) {
// Returns: // Returns:
// - An error if saving the expired sessions fails (only if w is not nil). // - 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 { 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 // Use defer to guarantee session is returned to pool regardless of any errors or panics
defer func() { defer func() {
@@ -1125,7 +1152,7 @@ func (sd *SessionData) SetAccessToken(token string) {
// Compress token with validation // Compress token with validation
compressed := compressToken(token) compressed := compressToken(token)
// FIXED: Size validation after compression // Validate size after compression
if len(compressed) > 100*1024 { // 100KB limit for final token (post-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)) sd.manager.logger.Info("Access token too large after compression (%d bytes) - storing uncompressed", len(compressed))
return return
+57 -12
View File
@@ -11,7 +11,9 @@ import (
"github.com/gorilla/sessions" "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 TokenConfig struct {
Type string Type string
MinLength int 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 { type TokenRetrievalResult struct {
Token string Token string
Error error 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 { type ChunkManager struct {
logger *Logger logger *Logger
mutex *sync.RWMutex 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 { func NewChunkManager(logger *Logger) *ChunkManager {
if logger == nil { if logger == nil {
logger = newNoOpLogger() 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 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( func (cm *ChunkManager) GetToken(
singleToken string, singleToken string,
compressed bool, compressed bool,
@@ -102,7 +128,17 @@ func (cm *ChunkManager) GetToken(
return cm.processChunkedToken(chunks, config) 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 { func (cm *ChunkManager) processSingleToken(token string, compressed bool, config TokenConfig) TokenRetrievalResult {
// Detect corruption markers // Detect corruption markers
if isCorruptionMarker(token) { if isCorruptionMarker(token) {
@@ -130,19 +166,28 @@ func (cm *ChunkManager) processSingleToken(token string, compressed bool, config
return cm.validateToken(finalToken, 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 { 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 { if sizeErr := cm.validateTokenSize(token, config); sizeErr != nil {
return TokenRetrievalResult{Token: "", Error: sizeErr} 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 { if chunkErr := cm.validateChunkingEfficiency(token, config); chunkErr != nil {
return TokenRetrievalResult{Token: "", Error: chunkErr} return TokenRetrievalResult{Token: "", Error: chunkErr}
} }
// Comprehensive content validation // Validate token content and structure
if contentErr := cm.validateTokenContent(token, config); contentErr != nil { if contentErr := cm.validateTokenContent(token, config); contentErr != nil {
return TokenRetrievalResult{Token: "", Error: contentErr} return TokenRetrievalResult{Token: "", Error: contentErr}
} }
@@ -157,7 +202,7 @@ func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRet
return TokenRetrievalResult{Token: "", Error: freshnessErr} return TokenRetrievalResult{Token: "", Error: freshnessErr}
} }
// Enhanced JWT format validation // Validate JWT format if required
if config.RequireJWTFormat && !config.AllowOpaqueTokens { if config.RequireJWTFormat && !config.AllowOpaqueTokens {
if validationErr := cm.validateJWTFormat(token, config.Type); validationErr != nil { if validationErr := cm.validateJWTFormat(token, config.Type); validationErr != nil {
return TokenRetrievalResult{Token: "", Error: validationErr} 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 // processChunkedToken handles tokens stored across multiple chunks
func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, config TokenConfig) TokenRetrievalResult { 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 { if len(chunks) > config.MaxChunks {
err := fmt.Errorf("too many %s token chunks (%d, max: %d)", config.Type, 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)) 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} return TokenRetrievalResult{Token: "", Error: err}
} }
// Enhanced chunk size validation using config limits // Validate individual chunk sizes
if len(chunk) > config.MaxChunkSize { if len(chunk) > config.MaxChunkSize {
err := fmt.Errorf("%s token chunk %d exceeds size limit (%d bytes, max: %d)", err := fmt.Errorf("%s token chunk %d exceeds size limit (%d bytes, max: %d)",
config.Type, i, len(chunk), config.MaxChunkSize) config.Type, i, len(chunk), config.MaxChunkSize)
+10 -10
View File
@@ -181,7 +181,7 @@ func (c *Config) Validate() error {
return fmt.Errorf("refreshGracePeriodSeconds cannot be negative") 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 { for _, header := range c.Headers {
if header.Name == "" { if header.Name == "" {
return fmt.Errorf("header name cannot be empty") 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) 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 { if err := validateTemplateSecure(header.Value); err != nil {
return fmt.Errorf("header template '%s' failed security validation: %w", header.Value, err) return fmt.Errorf("header template '%s' failed security validation: %w", header.Value, err)
} }
@@ -216,9 +216,9 @@ func (c *Config) Validate() error {
return nil return nil
} }
// SECURITY FIX: validateTemplateSecure implements template sandboxing and validation // validateTemplateSecure validates template expressions for security vulnerabilities
func validateTemplateSecure(templateStr string) error { func validateTemplateSecure(templateStr string) error {
// SECURITY FIX: Restrict dangerous template functions and patterns // Check for dangerous template functions and patterns
dangerousPatterns := []string{ dangerousPatterns := []string{
"{{call", // Function calls "{{call", // Function calls
"{{range", // Range over arbitrary data "{{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{ allowedPatterns := []string{
"{{.AccessToken}}", "{{.AccessToken}}",
"{{.IdToken}}", "{{.IdToken}}",
@@ -277,7 +277,7 @@ func validateTemplateSecure(templateStr string) error {
return fmt.Errorf("template must use only allowed variables: AccessToken, IdToken, RefreshToken, or Claims.*") 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.") { if strings.Contains(templateStr, "{{.Claims.") {
// Simple validation - ensure claims access is to known safe fields // Simple validation - ensure claims access is to known safe fields
safeClaimsFields := map[string]bool{ 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) 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.") nextStart := strings.Index(templateStr[start+end+2:], "{{.Claims.")
if nextStart != -1 { if nextStart != -1 {
start = start + end + 2 + nextStart 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, "}}") { if strings.Contains(templateStr, "{{") && strings.Contains(templateStr, "}}") {
// Count opening and closing braces // Count opening and closing braces
openCount := strings.Count(templateStr, "{{") openCount := strings.Count(templateStr, "{{")
@@ -418,7 +418,7 @@ func (l *Logger) Info(format string, args ...interface{}) {
l.logInfo.Printf(format, args...) 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". // Output is directed to stdout only if the configured log level is "debug".
// //
// Parameters: // Parameters:
@@ -449,7 +449,7 @@ func (l *Logger) Infof(format string, args ...interface{}) {
l.logInfo.Printf(format, args...) 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...). // Equivalent to calling l.Debug(format, args...).
// Output is directed to stdout only if the configured log level is "debug". // Output is directed to stdout only if the configured log level is "debug".
// //