mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
fixup! fixup! fixup! fixup! fixup! Cleanup tests.
This commit is contained in:
+20
-5
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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,}$`)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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".
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user