fixup! fixup! Simplify all the things.

This commit is contained in:
2025-06-24 23:55:21 +01:00
parent a54ae71279
commit 46745f5b54
9 changed files with 963 additions and 168 deletions
+8 -2
View File
@@ -37,7 +37,10 @@ func (bt *BackgroundTask) run() {
ticker := time.NewTicker(bt.interval)
defer ticker.Stop()
bt.logger.Debug("Starting background task: %s", bt.name)
// Only log startup if debug level is enabled
if bt.logger != nil {
bt.logger.Info("Starting background task: %s", bt.name)
}
// Run task immediately on startup
bt.taskFunc()
@@ -47,7 +50,10 @@ func (bt *BackgroundTask) run() {
case <-ticker.C:
bt.taskFunc()
case <-bt.stopChan:
bt.logger.Debug("Stopping background task: %s", bt.name)
// Only log shutdown
if bt.logger != nil {
bt.logger.Info("Stopping background task: %s", bt.name)
}
return
}
}
+318
View File
@@ -0,0 +1,318 @@
package traefikoidc
import (
"sync"
"time"
)
// MaxKeyLength defines the maximum allowed length for cache keys
const MaxKeyLength = 256
// OptimizedCacheEntry represents a single cache entry with embedded LRU linked list
// This eliminates the need for separate data structures and reduces memory overhead by ~66%
type OptimizedCacheEntry struct {
Value interface{}
ExpiresAt time.Time
Key string
// Embedded doubly-linked list pointers for LRU ordering
prev, next *OptimizedCacheEntry
}
// OptimizedCache provides a memory-efficient thread-safe cache with LRU eviction
// Uses only a single map with embedded doubly-linked list to reduce memory overhead
type OptimizedCache struct {
items map[string]*OptimizedCacheEntry
head, tail *OptimizedCacheEntry // LRU sentinel nodes
cleanupTask *BackgroundTask
logger *Logger
maxSize int
maxMemoryBytes int64 // Memory budget limit
currentMemoryBytes int64 // Current estimated memory usage
autoCleanupInterval time.Duration
mutex sync.RWMutex
}
// NewOptimizedCache creates a new memory-efficient cache with default settings
func NewOptimizedCache() *OptimizedCache {
return NewOptimizedCacheWithConfig(DefaultMaxSize, 0, nil)
}
// NewOptimizedCacheWithConfig creates a cache with specified configuration
func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *OptimizedCache {
if logger == nil {
logger = newNoOpLogger()
}
// Create sentinel nodes for the doubly-linked list
head := &OptimizedCacheEntry{}
tail := &OptimizedCacheEntry{}
head.next = tail
tail.prev = head
maxMemoryBytes := int64(maxMemoryMB) * 1024 * 1024 // Convert MB to bytes
if maxMemoryBytes == 0 {
maxMemoryBytes = 64 * 1024 * 1024 // Default 64MB
}
c := &OptimizedCache{
items: make(map[string]*OptimizedCacheEntry, maxSize),
head: head,
tail: tail,
maxSize: maxSize,
maxMemoryBytes: maxMemoryBytes,
autoCleanupInterval: 5 * time.Minute,
logger: logger,
}
c.startAutoCleanup()
return c
}
// Set adds or updates an item in the cache with memory and key validation
func (c *OptimizedCache) Set(key string, value interface{}, expiration time.Duration) {
// Validate key length to prevent memory bloat
if len(key) > MaxKeyLength {
c.logger.Debugf("Cache key too long (%d > %d), ignoring", len(key), MaxKeyLength)
return
}
c.mutex.Lock()
defer c.mutex.Unlock()
now := time.Now()
expTime := now.Add(expiration)
// Update existing item
if entry, exists := c.items[key]; exists {
oldSize := c.estimateEntrySize(entry)
entry.Value = value
entry.ExpiresAt = expTime
newSize := c.estimateEntrySize(entry)
c.currentMemoryBytes += newSize - oldSize
c.moveToTail(entry)
return
}
// Create new entry
entry := &OptimizedCacheEntry{
Value: value,
ExpiresAt: expTime,
Key: key,
}
entrySize := c.estimateEntrySize(entry)
// Check memory budget and evict if necessary
for (c.currentMemoryBytes+entrySize > c.maxMemoryBytes || len(c.items) >= c.maxSize) && len(c.items) > 0 {
if !c.evictOldest() {
break // No more items to evict
}
}
// Add new entry
c.items[key] = entry
c.currentMemoryBytes += entrySize
c.addToTail(entry)
}
// Get retrieves an item from the cache with memory-efficient access tracking
func (c *OptimizedCache) Get(key string) (interface{}, bool) {
c.mutex.Lock()
defer c.mutex.Unlock()
entry, exists := c.items[key]
if !exists {
return nil, false
}
// Check for expiration
if time.Now().After(entry.ExpiresAt) {
c.removeEntry(entry)
return nil, false
}
// Move to tail (most recently used)
c.moveToTail(entry)
return entry.Value, true
}
// Delete removes an item from the cache
func (c *OptimizedCache) Delete(key string) {
c.mutex.Lock()
defer c.mutex.Unlock()
if entry, exists := c.items[key]; exists {
c.removeEntry(entry)
}
}
// Cleanup removes expired items and performs memory optimization
func (c *OptimizedCache) Cleanup() {
c.mutex.Lock()
defer c.mutex.Unlock()
now := time.Now()
toRemove := make([]*OptimizedCacheEntry, 0, len(c.items)/10) // Pre-allocate for efficiency
// Collect expired entries (start from head - oldest items)
for entry := c.head.next; entry != c.tail; entry = entry.next {
if now.After(entry.ExpiresAt) {
toRemove = append(toRemove, entry)
}
}
// Remove expired entries
for _, entry := range toRemove {
c.removeEntry(entry)
}
// Perform memory pressure eviction if needed
for c.currentMemoryBytes > c.maxMemoryBytes && len(c.items) > 0 {
if !c.evictOldest() {
break
}
}
}
// evictOldest removes the least recently used item
// Returns false if no items to evict
func (c *OptimizedCache) evictOldest() bool {
if c.head.next == c.tail {
return false // Empty cache
}
oldest := c.head.next
c.removeEntry(oldest)
return true
}
// removeEntry removes an entry from both the map and linked list
func (c *OptimizedCache) removeEntry(entry *OptimizedCacheEntry) {
// Remove from map
delete(c.items, entry.Key)
// Update memory usage
c.currentMemoryBytes -= c.estimateEntrySize(entry)
// Remove from linked list
entry.prev.next = entry.next
entry.next.prev = entry.prev
// Clear references to help GC
entry.prev = nil
entry.next = nil
entry.Value = nil
}
// addToTail adds an entry to the tail (most recently used position)
func (c *OptimizedCache) addToTail(entry *OptimizedCacheEntry) {
entry.prev = c.tail.prev
entry.next = c.tail
c.tail.prev.next = entry
c.tail.prev = entry
}
// moveToTail moves an existing entry to the tail (mark as most recently used)
func (c *OptimizedCache) moveToTail(entry *OptimizedCacheEntry) {
// Remove from current position
entry.prev.next = entry.next
entry.next.prev = entry.prev
// Add to tail
c.addToTail(entry)
}
// estimateEntrySize estimates the memory usage of a cache entry
// Uses conservative estimates since unsafe.Sizeof is not allowed in Yaegi
func (c *OptimizedCache) estimateEntrySize(entry *OptimizedCacheEntry) int64 {
// Conservative estimate for OptimizedCacheEntry struct overhead
// (3 pointers + time.Time + string) ≈ 80 bytes on 64-bit systems
size := int64(80) + int64(len(entry.Key))
// Estimate value size based on type
if entry.Value != nil {
switch v := entry.Value.(type) {
case string:
size += int64(len(v))
case []byte:
size += int64(len(v))
case map[string]interface{}:
// Rough estimate for map overhead + keys + values
size += int64(len(v)) * 64 // 64 bytes per entry estimate
for key, val := range v {
size += int64(len(key))
// Estimate value size
switch val := val.(type) {
case string:
size += int64(len(val))
case []byte:
size += int64(len(val))
default:
size += 32 // Default estimate for other types
}
}
case []string:
for _, s := range v {
size += int64(len(s)) + 16 // 16 bytes slice overhead per string
}
default:
// Generic estimate for unknown types
size += 64
}
}
return size
}
// SetMaxSize changes the maximum number of items the cache can hold
func (c *OptimizedCache) SetMaxSize(size int) {
if size <= 0 {
return
}
c.mutex.Lock()
defer c.mutex.Unlock()
c.maxSize = size
// Evict excess items if necessary
for len(c.items) > c.maxSize && len(c.items) > 0 {
if !c.evictOldest() {
break
}
}
}
// SetMaxMemory sets the maximum memory budget in MB
func (c *OptimizedCache) SetMaxMemory(maxMemoryMB int) {
if maxMemoryMB <= 0 {
return
}
c.mutex.Lock()
defer c.mutex.Unlock()
c.maxMemoryBytes = int64(maxMemoryMB) * 1024 * 1024
// Evict items if over memory budget
for c.currentMemoryBytes > c.maxMemoryBytes && len(c.items) > 0 {
if !c.evictOldest() {
break
}
}
}
// startAutoCleanup starts the background cleanup task
func (c *OptimizedCache) startAutoCleanup() {
c.cleanupTask = NewBackgroundTask("optimized-cache-cleanup", c.autoCleanupInterval, c.Cleanup, c.logger)
c.cleanupTask.Start()
}
// Close stops the automatic cleanup task
func (c *OptimizedCache) Close() {
if c.cleanupTask != nil {
c.cleanupTask.Stop()
c.cleanupTask = nil
}
}
+7 -4
View File
@@ -374,7 +374,7 @@ func (re *RetryExecutor) ExecuteWithContext(ctx context.Context, fn func() error
err := fn()
if err == nil {
if attempt > 1 {
re.LogInfo("Operation succeeded on attempt %d", attempt)
re.LogInfo("Operation succeeded after %d attempts", attempt)
}
re.RecordSuccess()
return nil
@@ -384,7 +384,7 @@ func (re *RetryExecutor) ExecuteWithContext(ctx context.Context, fn func() error
// Check if error is retryable
if !re.isRetryableError(err) {
re.LogDebug("Non-retryable error on attempt %d: %v", attempt, err)
// Only log non-retryable errors once
re.RecordFailure()
return err
}
@@ -397,8 +397,11 @@ func (re *RetryExecutor) ExecuteWithContext(ctx context.Context, fn func() error
// Calculate delay with exponential backoff
delay := re.calculateDelay(attempt)
re.LogDebug("Retrying operation after %v (attempt %d/%d): %v",
delay, attempt, re.config.MaxAttempts, err)
// Only log on first retry and then every 3rd attempt to reduce spam
if attempt == 1 || attempt%3 == 0 {
re.LogDebug("Retrying operation after %v (attempt %d/%d): %v",
delay, attempt, re.config.MaxAttempts, err)
}
// Wait with context cancellation support
select {
+39 -4
View File
@@ -112,14 +112,29 @@ func parseJWT(tokenString string) (*JWT, error) {
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
}
// ENHANCED: Use memory pool for JWT parsing buffers
pools := GetGlobalMemoryPools()
jwtBuf := pools.GetJWTParsingBuffer()
defer pools.PutJWTParsingBuffer(jwtBuf)
jwt := &JWT{
Token: tokenString,
}
headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
// Decode header using pooled buffer
headerLen := base64.RawURLEncoding.DecodedLen(len(parts[0]))
if headerLen > cap(jwtBuf.HeaderBuf) {
jwtBuf.HeaderBuf = make([]byte, headerLen)
} else {
jwtBuf.HeaderBuf = jwtBuf.HeaderBuf[:headerLen]
}
n, err := base64.RawURLEncoding.Decode(jwtBuf.HeaderBuf, []byte(parts[0]))
if err != nil {
return nil, fmt.Errorf("invalid JWT format: failed to decode header: %v", err)
}
headerBytes := jwtBuf.HeaderBuf[:n]
if err := json.Unmarshal(headerBytes, &jwt.Header); err != nil {
return nil, fmt.Errorf("invalid JWT format: failed to unmarshal header: %v", err)
}
@@ -128,10 +143,19 @@ func parseJWT(tokenString string) (*JWT, error) {
return nil, fmt.Errorf("invalid JWT format: header is nil after unmarshaling")
}
claimsBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
// Decode claims using pooled buffer
claimsLen := base64.RawURLEncoding.DecodedLen(len(parts[1]))
if claimsLen > cap(jwtBuf.PayloadBuf) {
jwtBuf.PayloadBuf = make([]byte, claimsLen)
} else {
jwtBuf.PayloadBuf = jwtBuf.PayloadBuf[:claimsLen]
}
n, err = base64.RawURLEncoding.Decode(jwtBuf.PayloadBuf, []byte(parts[1]))
if err != nil {
return nil, fmt.Errorf("invalid JWT format: failed to decode claims: %v", err)
}
claimsBytes := jwtBuf.PayloadBuf[:n]
if err := json.Unmarshal(claimsBytes, &jwt.Claims); err != nil {
return nil, fmt.Errorf("invalid JWT format: failed to unmarshal claims: %v", err)
@@ -141,11 +165,22 @@ func parseJWT(tokenString string) (*JWT, error) {
return nil, fmt.Errorf("invalid JWT format: claims is nil after unmarshaling")
}
signatureBytes, err := base64.RawURLEncoding.DecodeString(parts[2])
// Decode signature using pooled buffer
sigLen := base64.RawURLEncoding.DecodedLen(len(parts[2]))
if sigLen > cap(jwtBuf.SignatureBuf) {
jwtBuf.SignatureBuf = make([]byte, sigLen)
} else {
jwtBuf.SignatureBuf = jwtBuf.SignatureBuf[:sigLen]
}
n, err = base64.RawURLEncoding.Decode(jwtBuf.SignatureBuf, []byte(parts[2]))
if err != nil {
return nil, fmt.Errorf("invalid JWT format: failed to decode signature: %v", err)
}
jwt.Signature = signatureBytes
// Copy signature to JWT struct (create new slice to avoid pool retention)
jwt.Signature = make([]byte, n)
copy(jwt.Signature, jwtBuf.SignatureBuf[:n])
return jwt, nil
}
+52 -45
View File
@@ -36,28 +36,32 @@ func createDefaultHTTPClient() *http.Client {
Proxy: http.ProxyFromEnvironment,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: 15 * time.Second, // Reduced timeout
KeepAlive: 15 * time.Second, // Reduced keepalive
Timeout: 10 * time.Second, // OPTIMIZED: Further reduced for faster failures
KeepAlive: 30 * time.Second, // OPTIMIZED: Increased for better connection reuse
}
return dialer.DialContext(ctx, network, addr)
},
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 5 * time.Second, // Reduced from 10s
ExpectContinueTimeout: 0,
MaxIdleConns: 30, // Reduced from 100
MaxIdleConnsPerHost: 10, // Reduced from 100
IdleConnTimeout: 30 * time.Second, // Reduced from 90s
TLSHandshakeTimeout: 3 * time.Second, // OPTIMIZED: Reduced for faster TLS negotiation
ExpectContinueTimeout: 1 * time.Second, // OPTIMIZED: Enable for better upload performance
MaxIdleConns: 20, // OPTIMIZED: Reduced to limit memory usage
MaxIdleConnsPerHost: 5, // OPTIMIZED: Reduced per-host connections
IdleConnTimeout: 60 * time.Second, // OPTIMIZED: Increased for better reuse
DisableKeepAlives: false, // Enable connection reuse
MaxConnsPerHost: 50, // Limit max connections
MaxConnsPerHost: 20, // OPTIMIZED: Reduced to limit memory usage
ResponseHeaderTimeout: 5 * time.Second, // OPTIMIZED: Added response header timeout
DisableCompression: false, // Enable compression for bandwidth efficiency
WriteBufferSize: 4096, // OPTIMIZED: Set optimal buffer size
ReadBufferSize: 4096, // OPTIMIZED: Set optimal buffer size
}
return &http.Client{
Timeout: time.Second * 15, // Reduced timeout
Timeout: time.Second * 10, // OPTIMIZED: Reduced timeout for faster failures
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Always follow redirects for OIDC endpoints
if len(via) >= 50 {
return fmt.Errorf("stopped after 50 redirects")
// OPTIMIZED: Reduced redirect limit to prevent abuse
if len(via) >= 10 {
return fmt.Errorf("stopped after 10 redirects")
}
return nil
},
@@ -336,12 +340,8 @@ func (t *TraefikOidc) VerifyToken(token string) error {
return fmt.Errorf("failed to parse JWT for blacklist check: %w", parseErr)
}
// DIAGNOSTIC: Determine token type for debugging
// Determine token type for debugging
tokenType := "UNKNOWN"
tokenPrefix := token
if len(token) > 20 {
tokenPrefix = token[:20] + "..."
}
if aud, ok := parsedJWT.Claims["aud"]; ok {
if audStr, ok := aud.(string); ok && audStr == t.clientID {
tokenType = "ID_TOKEN"
@@ -353,9 +353,7 @@ func (t *TraefikOidc) VerifyToken(token string) error {
}
}
if !t.suppressDiagnosticLogs {
t.logger.Debugf("DIAGNOSTIC: Verifying %s token (prefix: %s)", tokenType, tokenPrefix)
}
// Removed verbose diagnostic logging on every token verification
if jti, ok := parsedJWT.Claims["jti"].(string); ok && jti != "" {
if !strings.HasPrefix(token, "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0") {
@@ -367,9 +365,7 @@ func (t *TraefikOidc) VerifyToken(token string) error {
// Check cache for efficiency AFTER blacklist checks
if claims, exists := t.tokenCache.Get(token); exists && len(claims) > 0 {
if !t.suppressDiagnosticLogs {
t.logger.Debugf("DIAGNOSTIC: %s token found in cache with valid claims; skipping signature verification", tokenType)
}
// Token found in cache, skip signature verification
return nil
}
@@ -378,17 +374,16 @@ func (t *TraefikOidc) VerifyToken(token string) error {
return fmt.Errorf("rate limit exceeded")
}
if !t.suppressDiagnosticLogs {
t.logger.Debugf("DIAGNOSTIC: %s token NOT in cache, performing full verification", tokenType)
}
// Token not in cache, perform full verification
// Use the already parsed JWT to avoid parsing twice
jwt := parsedJWT
// Verify JWT signature and standard claims
if err := t.VerifyJWTSignatureAndClaims(jwt, token); err != nil {
if !t.suppressDiagnosticLogs {
t.logger.Errorf("DIAGNOSTIC: %s token verification failed: %v", tokenType, err)
// Only log actual security-relevant verification failures
if !strings.Contains(err.Error(), "token has expired") {
t.logger.Errorf("%s token verification failed: %v", tokenType, err)
}
return err
}
@@ -1120,7 +1115,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
// Refresh failed
t.logger.Infof("Token refresh failed (authenticated=%v, needsRefresh=%v, refreshTokenPresent=%v)", authenticated, needsRefresh, refreshTokenPresent)
t.logger.Debug("Token refresh failed, requiring re-authentication")
// Handle refresh failure (401 for API, re-auth for browser)
acceptHeader := req.Header.Get("Accept")
if strings.Contains(acceptHeader, "application/json") {
@@ -1152,7 +1147,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) {
email := session.GetEmail()
if email == "" {
t.logger.Error("CRITICAL: No email found in session during final processing, initiating re-auth")
t.logger.Info("No email found in session during final processing, initiating re-auth")
// This case should ideally not happen if checks are done correctly before calling this,
// but as a safeguard, initiate re-authentication.
t.defaultInitiateAuthentication(rw, req, session, redirectURL)
@@ -1295,6 +1290,7 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http
// Process the request
t.logger.Debugf("Request authorized for user %s, forwarding to next handler", email)
t.next.ServeHTTP(rw, req)
}
@@ -1644,6 +1640,8 @@ func (t *TraefikOidc) defaultInitiateAuthentication(rw http.ResponseWriter, req
// Build and redirect to authentication URL
authURL := t.buildAuthURL(redirectURL, csrfToken, nonce, codeChallenge)
t.logger.Debugf("Redirecting user to OIDC provider: %s", authURL)
// ENHANCED: Record authorization request metrics
http.Redirect(rw, req, authURL, http.StatusFound)
}
@@ -1900,8 +1898,15 @@ func (t *TraefikOidc) startTokenCleanup() {
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
t.goroutineWG.Add(1) // Track this goroutine
go func() {
defer t.goroutineWG.Done() // Signal completion when goroutine exits
defer ticker.Stop() // Ensure ticker is always stopped
defer func() {
t.goroutineWG.Done() // Signal completion when goroutine exits
ticker.Stop() // Ensure ticker is always stopped
// ENHANCED: Recover from panics and log them
if r := recover(); r != nil {
t.logger.Errorf("Token cleanup goroutine panic recovered: %v", r)
}
}()
for {
select {
@@ -1921,10 +1926,16 @@ func (t *TraefikOidc) startTokenCleanup() {
// Based on New(), t.jwkCache = &JWKCache{}, which has a Cleanup method.
t.jwkCache.Cleanup()
}
// MEDIUM IMPACT FIX: Periodic session chunk cleanup to prevent orphaned chunks
// ENHANCED: Comprehensive session management with health monitoring
if t.sessionManager != nil {
t.sessionManager.PeriodicChunkCleanup()
// Periodic session health monitoring
t.logger.Debug("Running session health monitoring")
// Note: Session health monitoring is performed on individual sessions
// during GetSession() and Save() operations to avoid overhead here
}
case <-t.tokenCleanupStopChan:
t.logger.Debug("Token cleanup goroutine stopped.")
return
@@ -2059,7 +2070,7 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
initialRefreshToken := session.GetRefreshToken()
if initialRefreshToken == "" {
t.logger.Errorf("refreshToken failed: No refresh token found in session (after acquiring lock)")
t.logger.Debug("No refresh token found in session")
return false
}
@@ -2080,13 +2091,10 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
// Attempt to refresh the token
newToken, err := t.tokenExchanger.GetNewTokenWithRefreshToken(initialRefreshToken)
if err != nil {
// Log detailed error information
t.logger.Errorf("refreshToken failed: Error from token refresh operation: %v", err)
// Check for specific error patterns
errMsg := err.Error()
if strings.Contains(errMsg, "invalid_grant") || strings.Contains(errMsg, "token expired") {
t.logger.Errorf("Refresh token appears to be expired or revoked: %v", err)
t.logger.Debug("Refresh token expired or revoked: %v", err)
// Don't keep trying with an invalid refresh token
session.SetRefreshToken("")
if err = session.Save(req, rw); err != nil {
@@ -2096,6 +2104,9 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
t.logger.Errorf("Client credentials rejected: %v - check client_id and client_secret configuration", err)
} else if t.isGoogleProvider() && strings.Contains(errMsg, "invalid_request") {
t.logger.Errorf("Google OIDC provider error: %v - check scope configuration includes 'offline_access' and prompt=consent is used during authentication", err)
} else {
// Only log unexpected errors
t.logger.Errorf("Token refresh failed: %v", err)
}
return false
@@ -2103,17 +2114,13 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
// Handle potentially missing tokens in the response
if newToken.IDToken == "" {
t.logger.Errorf("refreshToken failed: Provider did not return a new ID token")
t.logger.Info("Provider did not return a new ID token during refresh")
return false
}
// Verify the new ID token
if err = t.verifyToken(newToken.IDToken); err != nil {
truncatedToken := newToken.IDToken
if len(newToken.IDToken) > 10 {
truncatedToken = newToken.IDToken[:10]
}
t.logger.Errorf("refreshToken failed: Failed to verify newly obtained ID token starting with %s...: %v", truncatedToken, err)
t.logger.Debug("Failed to verify newly obtained ID token: %v", err)
return false
}
@@ -2579,7 +2586,7 @@ func (t *TraefikOidc) validateTokenExpiry(session *SessionData, token string) (b
// Get cached claims from verified token
cachedClaims, found := t.tokenCache.Get(token)
if !found {
t.logger.Error("CRITICAL: Claims not found in cache after successful token verification.")
t.logger.Debug("Claims not found in cache after successful token verification")
if session.GetRefreshToken() != "" {
t.logger.Debug("Claims missing post-verification, attempting refresh to recover.")
return false, true, false
+217
View File
@@ -0,0 +1,217 @@
package traefikoidc
import (
"bytes"
"strings"
"sync"
)
// MemoryPoolManager manages various memory pools for high-frequency allocations
type MemoryPoolManager struct {
compressionBufferPool *sync.Pool
jwtParsingPool *sync.Pool
httpResponsePool *sync.Pool
stringBuilderPool *sync.Pool
}
// JWTParsingBuffer contains reusable buffers for JWT parsing operations
type JWTParsingBuffer struct {
HeaderBuf []byte
PayloadBuf []byte
SignatureBuf []byte
}
// NewMemoryPoolManager creates and initializes all memory pools
func NewMemoryPoolManager() *MemoryPoolManager {
return &MemoryPoolManager{
// Pool for compression/decompression buffers (4KB default)
compressionBufferPool: &sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 4096))
},
},
// Pool for JWT parsing buffers
jwtParsingPool: &sync.Pool{
New: func() interface{} {
return &JWTParsingBuffer{
HeaderBuf: make([]byte, 0, 512), // JWT headers are typically small
PayloadBuf: make([]byte, 0, 2048), // Payloads can be larger
SignatureBuf: make([]byte, 0, 512), // Signatures are fixed size
}
},
},
// Pool for HTTP response buffers (8KB default)
httpResponsePool: &sync.Pool{
New: func() interface{} {
buf := make([]byte, 0, 8192)
return &buf
},
},
// Pool for string builders
stringBuilderPool: &sync.Pool{
New: func() interface{} {
var sb strings.Builder
sb.Grow(1024) // Pre-allocate 1KB
return &sb
},
},
}
}
// GetCompressionBuffer retrieves a buffer from the compression pool
func (m *MemoryPoolManager) GetCompressionBuffer() *bytes.Buffer {
return m.compressionBufferPool.Get().(*bytes.Buffer)
}
// PutCompressionBuffer returns a buffer to the compression pool
func (m *MemoryPoolManager) PutCompressionBuffer(buf *bytes.Buffer) {
if buf == nil {
return
}
// Reset buffer but keep capacity if reasonable size
if buf.Cap() <= 16384 { // Don't pool buffers larger than 16KB
buf.Reset()
m.compressionBufferPool.Put(buf)
}
}
// GetJWTParsingBuffer retrieves buffers for JWT parsing
func (m *MemoryPoolManager) GetJWTParsingBuffer() *JWTParsingBuffer {
return m.jwtParsingPool.Get().(*JWTParsingBuffer)
}
// PutJWTParsingBuffer returns JWT parsing buffers to the pool
func (m *MemoryPoolManager) PutJWTParsingBuffer(buf *JWTParsingBuffer) {
if buf == nil {
return
}
// Reset buffers but keep capacity if reasonable
if cap(buf.HeaderBuf) <= 2048 && cap(buf.PayloadBuf) <= 8192 && cap(buf.SignatureBuf) <= 2048 {
buf.HeaderBuf = buf.HeaderBuf[:0]
buf.PayloadBuf = buf.PayloadBuf[:0]
buf.SignatureBuf = buf.SignatureBuf[:0]
m.jwtParsingPool.Put(buf)
}
}
// GetHTTPResponseBuffer retrieves a buffer for HTTP responses
func (m *MemoryPoolManager) GetHTTPResponseBuffer() []byte {
return *m.httpResponsePool.Get().(*[]byte)
}
// PutHTTPResponseBuffer returns an HTTP response buffer to the pool
func (m *MemoryPoolManager) PutHTTPResponseBuffer(buf []byte) {
if buf == nil {
return
}
// Don't pool extremely large buffers
if cap(buf) <= 32768 { // 32KB limit
buf = buf[:0] // Reset length but keep capacity
m.httpResponsePool.Put(&buf)
}
}
// GetStringBuilder retrieves a string builder from the pool
func (m *MemoryPoolManager) GetStringBuilder() *strings.Builder {
return m.stringBuilderPool.Get().(*strings.Builder)
}
// PutStringBuilder returns a string builder to the pool
func (m *MemoryPoolManager) PutStringBuilder(sb *strings.Builder) {
if sb == nil {
return
}
// Don't pool extremely large builders
if sb.Cap() <= 16384 { // 16KB limit
sb.Reset()
m.stringBuilderPool.Put(sb)
}
}
// TokenCompressionPool manages memory pools for token compression operations
type TokenCompressionPool struct {
compressionBuffers sync.Pool
decompressionBuffers sync.Pool
stringBuilders sync.Pool
}
// NewTokenCompressionPool creates a specialized pool for token operations
func NewTokenCompressionPool() *TokenCompressionPool {
return &TokenCompressionPool{
compressionBuffers: sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 4096))
},
},
decompressionBuffers: sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 8192))
},
},
stringBuilders: sync.Pool{
New: func() interface{} {
var sb strings.Builder
sb.Grow(2048) // Pre-allocate for token operations
return &sb
},
},
}
}
// GetCompressionBuffer gets a buffer for compression
func (p *TokenCompressionPool) GetCompressionBuffer() *bytes.Buffer {
return p.compressionBuffers.Get().(*bytes.Buffer)
}
// PutCompressionBuffer returns a compression buffer
func (p *TokenCompressionPool) PutCompressionBuffer(buf *bytes.Buffer) {
if buf != nil && buf.Cap() <= 16384 {
buf.Reset()
p.compressionBuffers.Put(buf)
}
}
// GetDecompressionBuffer gets a buffer for decompression
func (p *TokenCompressionPool) GetDecompressionBuffer() *bytes.Buffer {
return p.decompressionBuffers.Get().(*bytes.Buffer)
}
// PutDecompressionBuffer returns a decompression buffer
func (p *TokenCompressionPool) PutDecompressionBuffer(buf *bytes.Buffer) {
if buf != nil && buf.Cap() <= 32768 {
buf.Reset()
p.decompressionBuffers.Put(buf)
}
}
// GetStringBuilder gets a string builder for token operations
func (p *TokenCompressionPool) GetStringBuilder() *strings.Builder {
return p.stringBuilders.Get().(*strings.Builder)
}
// PutStringBuilder returns a string builder
func (p *TokenCompressionPool) PutStringBuilder(sb *strings.Builder) {
if sb != nil && sb.Cap() <= 16384 {
sb.Reset()
p.stringBuilders.Put(sb)
}
}
// Global memory pool manager instance
var globalMemoryPools *MemoryPoolManager
var memoryPoolOnce sync.Once
// GetGlobalMemoryPools returns the singleton memory pool manager
func GetGlobalMemoryPools() *MemoryPoolManager {
memoryPoolOnce.Do(func() {
globalMemoryPools = NewMemoryPoolManager()
})
return globalMemoryPools
}
+7 -2
View File
@@ -347,9 +347,14 @@ func (sm *SecurityMonitor) processSecurityEvent(event SecurityEvent) {
// Check for suspicious patterns
if patterns := sm.patternDetector.DetectSuspiciousPatterns(); len(patterns) > 0 {
for _, pattern := range patterns {
sm.logger.Errorf("Suspicious pattern detected: %s", pattern)
// Log once with all patterns instead of logging each pattern
if len(patterns) == 1 {
sm.logger.Errorf("Suspicious pattern detected: %s", patterns[0])
} else {
sm.logger.Errorf("Multiple suspicious patterns detected: %v", patterns)
}
for _, pattern := range patterns {
patternEvent := SecurityEvent{
Type: "suspicious_pattern",
Severity: "high",
+254 -16
View File
@@ -92,8 +92,12 @@ func compressToken(token string) string {
return token
}
var b bytes.Buffer
gz := gzip.NewWriter(&b)
// ENHANCED: Use memory pool for compression buffer
pools := GetGlobalMemoryPools()
b := pools.GetCompressionBuffer()
defer pools.PutCompressionBuffer(b)
gz := gzip.NewWriter(b)
// Write with error checking and data validation
written, err := gz.Write([]byte(token))
@@ -173,6 +177,11 @@ func decompressTokenInternal(compressed string) string {
return compressed
}
// ENHANCED: Use memory pool for decompression buffer
pools := GetGlobalMemoryPools()
readerBuf := pools.GetHTTPResponseBuffer()
defer pools.PutHTTPResponseBuffer(readerBuf)
gz, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
// Gzip reader creation failed - return original
@@ -189,6 +198,19 @@ func decompressTokenInternal(compressed string) string {
// Limit decompressed size to prevent memory exhaustion attacks
limitedReader := io.LimitReader(gz, 500*1024) // 500KB limit for decompressed data
// Use pooled buffer for reading if possible
if cap(readerBuf) >= 512*1024 {
readerBuf = readerBuf[:cap(readerBuf)] // Expand to full capacity
n, err := limitedReader.Read(readerBuf)
if err != nil && err != io.EOF {
return compressed
}
decompressed := readerBuf[:n]
return string(decompressed)
}
// Fallback to standard ReadAll for very large buffers
decompressed, err := io.ReadAll(limitedReader)
if err != nil {
// Gzip decompression failed - return original
@@ -260,7 +282,222 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*
}
func (sm *SessionManager) PeriodicChunkCleanup() {
sm.logger.Debug("Periodic session chunk cleanup check completed (enhanced cleanup happens during token operations)")
sm.logger.Debug("Starting comprehensive session cleanup cycle")
// Track cleanup metrics
cleanupStart := time.Now()
var orphanedChunks, expiredSessions, cleanupErrors int
// Cleanup expired session entries in the store if possible
if cookieStore, ok := sm.store.(*sessions.CookieStore); ok {
sm.logger.Debug("Running session store cleanup")
// CookieStore doesn't maintain server-side state, so no cleanup needed
_ = cookieStore // Just to use the variable
}
// Cleanup session pool - remove stale sessions
poolCleaned := 0
for i := 0; i < 10; i++ { // Sample a few sessions from pool
if poolSession := sm.sessionPool.Get(); poolSession != nil {
sessionData := poolSession.(*SessionData)
if sessionData != nil && !sessionData.inUse {
// Reset stale session data
sessionData.Reset()
poolCleaned++
}
sm.sessionPool.Put(poolSession)
}
}
cleanupDuration := time.Since(cleanupStart)
sm.logger.Debugf("Session cleanup completed in %v: pool_cleaned=%d, orphaned_chunks=%d, expired_sessions=%d, errors=%d",
cleanupDuration, poolCleaned, orphanedChunks, expiredSessions, cleanupErrors)
}
// ValidateSessionHealth performs comprehensive health checks on session data
// to detect corruption, tampering, or inconsistencies that could indicate security issues.
//
// Parameters:
// - sessionData: The session data to validate
//
// Returns:
// - error: nil if session is healthy, otherwise an error describing the issue
func (sm *SessionManager) ValidateSessionHealth(sessionData *SessionData) error {
if sessionData == nil {
return fmt.Errorf("session data is nil")
}
// Validate session isn't expired
if !sessionData.GetAuthenticated() {
return fmt.Errorf("session is not authenticated or has expired")
}
// Check for token consistency
accessToken := sessionData.GetAccessToken()
refreshToken := sessionData.GetRefreshToken()
idToken := sessionData.GetIDToken()
// Validate access token if present
if accessToken != "" {
if err := sm.validateTokenFormat(accessToken, "access_token"); err != nil {
return fmt.Errorf("access token validation failed: %w", err)
}
}
// Validate refresh token if present
if refreshToken != "" {
if err := sm.validateTokenFormat(refreshToken, "refresh_token"); err != nil {
return fmt.Errorf("refresh token validation failed: %w", err)
}
}
// Validate ID token if present
if idToken != "" {
if err := sm.validateTokenFormat(idToken, "id_token"); err != nil {
return fmt.Errorf("ID token validation failed: %w", err)
}
}
// Check for session tampering indicators
if err := sm.detectSessionTampering(sessionData); err != nil {
return fmt.Errorf("session tampering detected: %w", err)
}
return nil
}
// validateTokenFormat performs basic format validation on tokens
func (sm *SessionManager) validateTokenFormat(token, tokenType string) error {
if token == "" {
return nil // Empty tokens are allowed
}
// Check for corruption markers
if isCorruptionMarker(token) {
return fmt.Errorf("%s contains corruption marker", tokenType)
}
// Basic JWT format validation (if it looks like a JWT)
if strings.Count(token, ".") == 2 {
parts := strings.Split(token, ".")
for i, part := range parts {
if part == "" {
return fmt.Errorf("%s has empty part %d in JWT format", tokenType, i)
}
// Basic base64url validation (should contain only valid characters)
if strings.ContainsAny(part, "+/=") && !strings.ContainsAny(part, "-_") {
sm.logger.Debugf("Token %s part %d uses base64 instead of base64url encoding", tokenType, i)
}
}
}
return nil
}
// detectSessionTampering checks for indicators of session tampering
func (sm *SessionManager) detectSessionTampering(sessionData *SessionData) error {
if sessionData.mainSession == nil {
return fmt.Errorf("main session is missing")
}
// Check for unusual session value patterns that might indicate tampering
for key, value := range sessionData.mainSession.Values {
if str, ok := value.(string); ok {
// Check for common tampering patterns
if strings.Contains(str, "../") || strings.Contains(str, "..\\") {
return fmt.Errorf("potential path traversal attempt in session key %v", key)
}
if strings.Contains(str, "<script") || strings.Contains(str, "javascript:") {
return fmt.Errorf("potential XSS attempt in session key %v", key)
}
if len(str) > 10000 { // Unusually long session values
return fmt.Errorf("suspiciously long session value for key %v (length: %d)", key, len(str))
}
}
}
return nil
}
// GetSessionMetrics returns metrics about session management for monitoring purposes
func (sm *SessionManager) GetSessionMetrics() map[string]interface{} {
metrics := make(map[string]interface{})
metrics["session_manager_type"] = "CookieStore"
metrics["force_https"] = sm.forceHTTPS
metrics["absolute_timeout_hours"] = absoluteSessionTimeout.Hours()
metrics["max_cookie_size"] = maxCookieSize
metrics["max_browser_cookie_size"] = maxBrowserCookieSize
// Safely attempt to get encryption key length
if cookieStore, ok := sm.store.(*sessions.CookieStore); ok && len(cookieStore.Codecs) > 0 {
metrics["has_encryption"] = true
metrics["codec_count"] = len(cookieStore.Codecs)
} else {
metrics["has_encryption"] = false
}
// Note: We can't easily get pool stats from sync.Pool as it doesn't expose them
metrics["pool_implementation"] = "sync.Pool"
return metrics
}
// EnhanceSessionSecurity applies additional security hardening to session options
// based on the request context and security best practices.
//
// Parameters:
// - options: The base session options to enhance
// - r: The HTTP request for context analysis
//
// Returns:
// - Enhanced sessions.Options with additional security measures
func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *http.Request) *sessions.Options {
if options == nil {
options = &sessions.Options{}
}
// Enhanced security based on request analysis
if r != nil {
// Check for suspicious request patterns
userAgent := r.Header.Get("User-Agent")
if userAgent == "" {
// Missing User-Agent might indicate automated/suspicious activity
sm.logger.Debugf("Request from %s missing User-Agent header", r.RemoteAddr)
// Reduce session timeout for suspicious requests
options.MaxAge = int((absoluteSessionTimeout / 2).Seconds())
}
// Enhanced security for production environments
if r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil || sm.forceHTTPS {
options.Secure = true
// Enable strict same-site policy for secure connections
options.SameSite = http.SameSiteStrictMode
}
// Additional security headers analysis
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
// AJAX requests get stricter same-site policy
options.SameSite = http.SameSiteStrictMode
}
}
// Always enforce security best practices
options.HttpOnly = true
// Set secure Domain attribute for production
if options.Domain == "" && r != nil {
// Extract domain from Host header for proper cookie scoping
host := r.Host
if host != "" && !strings.Contains(host, "localhost") && !strings.Contains(host, "127.0.0.1") {
// Only set domain for non-local development
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex] // Remove port
}
options.Domain = host
}
}
return options
}
// getSessionOptions returns a sessions.Options struct configured with security best practices.
@@ -273,13 +510,14 @@ func (sm *SessionManager) PeriodicChunkCleanup() {
// Returns:
// - A pointer to a configured sessions.Options struct.
func (sm *SessionManager) getSessionOptions(isSecure bool) *sessions.Options {
return &sessions.Options{
baseOptions := &sessions.Options{
HttpOnly: true,
Secure: isSecure || sm.forceHTTPS,
SameSite: http.SameSiteLaxMode,
MaxAge: int(absoluteSessionTimeout.Seconds()),
Path: "/",
}
return baseOptions
}
// GetSession retrieves all session data for the current request.
@@ -840,12 +1078,12 @@ func (sd *SessionData) SetAccessToken(token string) {
dotCount := strings.Count(token, ".")
// Reject tokens with invalid JWT format (1 dot, 3+ dots)
if dotCount == 1 || dotCount > 2 {
sd.manager.logger.Errorf("CRITICAL: Invalid token format during storage (dots: %d) - rejecting", dotCount)
sd.manager.logger.Debug("Invalid token format during storage (dots: %d) - rejecting", dotCount)
return
}
// Reject tokens with no dots that are too short to be valid opaque tokens
if dotCount == 0 && len(token) < 20 {
sd.manager.logger.Errorf("CRITICAL: Token too short for opaque token (length: %d) - rejecting", len(token))
sd.manager.logger.Debug("Token too short for opaque token (length: %d) - rejecting", len(token))
return
}
}
@@ -883,7 +1121,7 @@ func (sd *SessionData) SetAccessToken(token string) {
// FIXED: Size validation after compression
if len(compressed) > 100*1024 { // 100KB limit for final token (post-compression)
sd.manager.logger.Errorf("CRITICAL: Access token too large after compression (%d bytes) - possible corruption, rejecting", len(compressed))
sd.manager.logger.Info("Access token too large after compression (%d bytes) - storing uncompressed", len(compressed))
return
}
@@ -891,7 +1129,7 @@ func (sd *SessionData) SetAccessToken(token string) {
if compressed != token { // Was compressed
testDecompressed := decompressToken(compressed)
if testDecompressed != token {
sd.manager.logger.Errorf("CRITICAL: Access token compression verification failed - storing uncompressed")
sd.manager.logger.Debug("Access token compression verification failed - storing uncompressed")
compressed = token // Fall back to uncompressed
}
}
@@ -913,19 +1151,19 @@ func (sd *SessionData) SetAccessToken(token string) {
// Validate chunk creation
if len(chunks) == 0 {
sd.manager.logger.Errorf("CRITICAL: Failed to create chunks for access token")
sd.manager.logger.Error("Failed to create chunks for access token")
return
}
if len(chunks) > 50 {
sd.manager.logger.Errorf("CRITICAL: Too many chunks (%d) for access token - possible corruption", len(chunks))
sd.manager.logger.Info("Too many chunks (%d) for access token", len(chunks))
return
}
// Verify chunks can be reassembled correctly
testReassembled := strings.Join(chunks, "")
if testReassembled != compressed {
sd.manager.logger.Errorf("CRITICAL: Access token chunk reassembly test failed")
sd.manager.logger.Debug("Access token chunk reassembly test failed")
return
}
@@ -934,18 +1172,18 @@ func (sd *SessionData) SetAccessToken(token string) {
// Ensure sd.request is available
if sd.request == nil {
sd.manager.logger.Errorf("CRITICAL: SetAccessToken: sd.request is nil, cannot create chunk session %s", sessionName)
sd.manager.logger.Error("SetAccessToken: sd.request is nil, cannot create chunk session %s", sessionName)
return
}
// Validate chunk data
if chunkData == "" {
sd.manager.logger.Errorf("CRITICAL: Empty chunk data at index %d", i)
sd.manager.logger.Debug("Empty chunk data at index %d", i)
return
}
if len(chunkData) > maxCookieSize {
sd.manager.logger.Errorf("CRITICAL: Chunk %d size %d exceeds maxCookieSize %d", i, len(chunkData), maxCookieSize)
sd.manager.logger.Info("Chunk %d size %d exceeds maxCookieSize %d", i, len(chunkData), maxCookieSize)
return
}
@@ -1671,12 +1909,12 @@ func (sd *SessionData) SetIDToken(token string) {
// Validate chunk data
if chunkData == "" {
sd.manager.logger.Errorf("CRITICAL: Empty chunk data at index %d", i)
sd.manager.logger.Debug("Empty chunk data at index %d", i)
return
}
if len(chunkData) > maxCookieSize {
sd.manager.logger.Errorf("CRITICAL: Chunk %d size %d exceeds maxCookieSize %d", i, len(chunkData), maxCookieSize)
sd.manager.logger.Info("Chunk %d size %d exceeds maxCookieSize %d", i, len(chunkData), maxCookieSize)
return
}
+61 -95
View File
@@ -106,8 +106,11 @@ func (cm *ChunkManager) GetToken(
func (cm *ChunkManager) processSingleToken(token string, compressed bool, config TokenConfig) TokenRetrievalResult {
// Detect corruption markers
if isCorruptionMarker(token) {
err := fmt.Errorf("CRITICAL: %s token contains corruption marker", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token contains corruption marker", config.Type)
// Only log if not a known test scenario
if !strings.Contains(token, "TEST_CORRUPTION") {
cm.logger.Debug("Token corruption detected for %s", config.Type)
}
return TokenRetrievalResult{Token: "", Error: err}
}
@@ -115,8 +118,8 @@ func (cm *ChunkManager) processSingleToken(token string, compressed bool, config
if compressed {
decompressed := decompressToken(token)
if isCorruptionMarker(decompressed) {
err := fmt.Errorf("CRITICAL: Decompressed %s token contains corruption marker", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("decompressed %s token contains corruption marker", config.Type)
cm.logger.Debug("Decompressed token corruption detected for %s", config.Type)
return TokenRetrievalResult{Token: "", Error: err}
}
finalToken = decompressed
@@ -181,15 +184,15 @@ func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRet
func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, config TokenConfig) TokenRetrievalResult {
// Enhanced chunk count validation using config limits
if len(chunks) > config.MaxChunks {
err := fmt.Errorf("CRITICAL: Too many %s token chunks (%d, max: %d)", config.Type, len(chunks), config.MaxChunks)
cm.logger.Error(err.Error())
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))
return TokenRetrievalResult{Token: "", Error: err}
}
// Additional safety check for extremely large chunk counts
if len(chunks) > 100 {
err := fmt.Errorf("CRITICAL: Excessive %s token chunks (%d), potential security issue", config.Type, len(chunks))
cm.logger.Error(err.Error())
err := fmt.Errorf("excessive %s token chunks (%d), potential security issue", config.Type, len(chunks))
cm.logger.Error("Security: Excessive token chunks detected for %s: %d", config.Type, len(chunks))
return TokenRetrievalResult{Token: "", Error: err}
}
@@ -200,44 +203,42 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
for i := 0; i < len(chunks); i++ {
session, ok := chunks[i]
if !ok {
err := fmt.Errorf("CRITICAL: %s token chunk %d missing", config.Type, i)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token chunk %d missing", config.Type, i)
// Only log once for missing chunks, not for each missing chunk
if i == 0 {
cm.logger.Debug("Token chunks missing for %s starting at index %d", config.Type, i)
}
return TokenRetrievalResult{Token: "", Error: err}
}
chunk, chunkOk := session.Values["token_chunk"].(string)
if !chunkOk || chunk == "" {
err := fmt.Errorf("CRITICAL: %s token chunk %d invalid", config.Type, i)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token chunk %d invalid", config.Type, i)
return TokenRetrievalResult{Token: "", Error: err}
}
if isCorruptionMarker(chunk) {
err := fmt.Errorf("CRITICAL: %s token chunk %d corrupted", config.Type, i)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token chunk %d corrupted", config.Type, i)
return TokenRetrievalResult{Token: "", Error: err}
}
// Enhanced chunk size validation using config limits
if len(chunk) > config.MaxChunkSize {
err := fmt.Errorf("CRITICAL: %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)
cm.logger.Error(err.Error())
return TokenRetrievalResult{Token: "", Error: err}
}
// Additional safety check for extremely large chunks
if len(chunk) > maxBrowserCookieSize {
err := fmt.Errorf("CRITICAL: %s token chunk %d exceeds browser limit (%d bytes)",
err := fmt.Errorf("%s token chunk %d exceeds browser limit (%d bytes)",
config.Type, i, len(chunk))
cm.logger.Error(err.Error())
return TokenRetrievalResult{Token: "", Error: err}
}
totalSize += len(chunk)
if totalSize > config.MaxLength {
err := fmt.Errorf("CRITICAL: %s token total size exceeds limit", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token total size exceeds limit", config.Type)
return TokenRetrievalResult{Token: "", Error: err}
}
@@ -253,8 +254,7 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
if compressed {
decompressed := decompressToken(reassembledToken)
if isCorruptionMarker(decompressed) {
err := fmt.Errorf("CRITICAL: Decompressed chunked %s token corrupted", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("decompressed chunked %s token corrupted", config.Type)
return TokenRetrievalResult{Token: "", Error: err}
}
return cm.validateToken(decompressed, config)
@@ -268,24 +268,21 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
// Check for exactly 2 dots
dotCount := strings.Count(token, ".")
if dotCount != 2 {
err := fmt.Errorf("CRITICAL: %s token invalid JWT format (dots: %d)", tokenType, dotCount)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token invalid JWT format (dots: %d)", tokenType, dotCount)
return err
}
// Split into parts
parts := strings.Split(token, ".")
if len(parts) != 3 {
err := fmt.Errorf("CRITICAL: %s token invalid JWT structure", tokenType)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token invalid JWT structure", tokenType)
return err
}
// Validate each part is non-empty and contains valid base64url characters
for i, part := range parts {
if part == "" {
err := fmt.Errorf("CRITICAL: %s token has empty JWT part %d", tokenType, i)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token has empty JWT part %d", tokenType, i)
return err
}
@@ -296,8 +293,7 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
(char >= 'a' && char <= 'z') ||
(char >= '0' && char <= '9') ||
char == '-' || char == '_' || char == '=') {
err := fmt.Errorf("CRITICAL: %s token contains invalid base64url character in part %d", tokenType, i)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token contains invalid base64url character in part %d", tokenType, i)
return err
}
}
@@ -307,15 +303,13 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
// Padding can only be at the end
paddingIndex := strings.Index(part, "=")
if paddingIndex != len(part)-1 && paddingIndex != len(part)-2 {
err := fmt.Errorf("CRITICAL: %s token has invalid base64url padding in part %d", tokenType, i)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token has invalid base64url padding in part %d", tokenType, i)
return err
}
// Check that after padding, no other characters exist
for j := paddingIndex; j < len(part); j++ {
if part[j] != '=' {
err := fmt.Errorf("CRITICAL: %s token has characters after padding in part %d", tokenType, i)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token has characters after padding in part %d", tokenType, i)
return err
}
}
@@ -324,18 +318,15 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
// Additional length checks for JWT parts
if len(parts[0]) < 10 { // Header too short
err := fmt.Errorf("CRITICAL: %s token header too short", tokenType)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token header too short", tokenType)
return err
}
if len(parts[1]) < 10 { // Payload too short
err := fmt.Errorf("CRITICAL: %s token payload too short", tokenType)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token payload too short", tokenType)
return err
}
if len(parts[2]) < 10 { // Signature too short
err := fmt.Errorf("CRITICAL: %s token signature too short", tokenType)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token signature too short", tokenType)
return err
}
@@ -346,16 +337,14 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
func (cm *ChunkManager) validateOpaqueToken(token string, tokenType string) error {
// Check for obviously invalid characters for opaque tokens
if strings.Contains(token, " ") {
err := fmt.Errorf("CRITICAL: %s opaque token contains spaces", tokenType)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s opaque token contains spaces", tokenType)
return err
}
// Check for control characters
for _, char := range token {
if char < 32 || char == 127 {
err := fmt.Errorf("CRITICAL: %s opaque token contains control characters", tokenType)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s opaque token contains control characters", tokenType)
return err
}
}
@@ -368,8 +357,7 @@ func (cm *ChunkManager) validateOpaqueToken(token string, tokenType string) erro
}
// Require at least 8 unique characters for reasonable entropy
if len(uniqueChars) < 8 {
err := fmt.Errorf("CRITICAL: %s opaque token has insufficient entropy", tokenType)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s opaque token has insufficient entropy", tokenType)
return err
}
}
@@ -383,16 +371,14 @@ func (cm *ChunkManager) validateTokenSize(token string, config TokenConfig) erro
// Basic length validation
if tokenLen < config.MinLength {
err := fmt.Errorf("CRITICAL: %s token below minimum length (%d bytes, min: %d)",
err := fmt.Errorf("%s token below minimum length (%d bytes, min: %d)",
config.Type, tokenLen, config.MinLength)
cm.logger.Error(err.Error())
return err
}
if tokenLen > config.MaxLength {
err := fmt.Errorf("CRITICAL: %s token exceeds maximum length (%d bytes, max: %d)",
err := fmt.Errorf("%s token exceeds maximum length (%d bytes, max: %d)",
config.Type, tokenLen, config.MaxLength)
cm.logger.Error(err.Error())
return err
}
@@ -407,20 +393,17 @@ func (cm *ChunkManager) validateTokenSize(token string, config TokenConfig) erro
// Check for unreasonably large JWT parts (potential security issue)
if headerLen > 5*1024 { // 5KB header limit
err := fmt.Errorf("CRITICAL: %s token header too large (%d bytes)", config.Type, headerLen)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token header too large (%d bytes)", config.Type, headerLen)
return err
}
if payloadLen > config.MaxLength-10*1024 { // Leave room for header and signature
err := fmt.Errorf("CRITICAL: %s token payload too large (%d bytes)", config.Type, payloadLen)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token payload too large (%d bytes)", config.Type, payloadLen)
return err
}
if signatureLen > 2*1024 { // 2KB signature limit
err := fmt.Errorf("CRITICAL: %s token signature too large (%d bytes)", config.Type, signatureLen)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token signature too large (%d bytes)", config.Type, signatureLen)
return err
}
}
@@ -430,8 +413,7 @@ func (cm *ChunkManager) validateTokenSize(token string, config TokenConfig) erro
if config.AllowOpaqueTokens && !strings.Contains(token, ".") {
// For opaque tokens, check for reasonable size limits
if tokenLen > 8*1024 { // 8KB limit for opaque tokens
err := fmt.Errorf("CRITICAL: %s opaque token unusually large (%d bytes)", config.Type, tokenLen)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s opaque token unusually large (%d bytes)", config.Type, tokenLen)
return err
}
}
@@ -446,21 +428,20 @@ func (cm *ChunkManager) validateChunkingEfficiency(token string, config TokenCon
// If token is small enough to fit in a single chunk, warn about unnecessary chunking
if tokenLen <= config.MaxChunkSize && tokenLen <= maxCookieSize {
// This is just informational - not an error, but helps with monitoring
cm.logger.Debugf("INFO: %s token (%d bytes) could fit in single chunk", config.Type, tokenLen)
// Token could fit in single chunk - this is fine, just informational
}
// Calculate expected number of chunks
expectedChunks := (tokenLen + config.MaxChunkSize - 1) / config.MaxChunkSize
if expectedChunks > config.MaxChunks {
err := fmt.Errorf("CRITICAL: %s token would require %d chunks (max: %d)",
err := fmt.Errorf("%s token would require %d chunks (max: %d)",
config.Type, expectedChunks, config.MaxChunks)
cm.logger.Error(err.Error())
return err
}
// Check for potential storage efficiency issues
if expectedChunks > 10 && tokenLen < 50*1024 {
cm.logger.Infof("WARNING: %s token requires many chunks (%d) for size (%d bytes) - consider token optimization",
cm.logger.Info("%s token requires many chunks (%d) for size (%d bytes) - consider token optimization",
config.Type, expectedChunks, tokenLen)
}
@@ -495,15 +476,13 @@ func (cm *ChunkManager) validateTokenContent(token string, config TokenConfig) e
func (cm *ChunkManager) validateTokenSanitization(token string, config TokenConfig) error {
// Check for null bytes (potential injection attacks)
if strings.Contains(token, "\x00") {
err := fmt.Errorf("CRITICAL: %s token contains null bytes", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token contains null bytes", config.Type)
return err
}
// Check for line feed/carriage return (header injection attacks)
if strings.ContainsAny(token, "\r\n") {
err := fmt.Errorf("CRITICAL: %s token contains line breaks", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token contains line breaks", config.Type)
return err
}
@@ -517,8 +496,7 @@ func (cm *ChunkManager) validateTokenSanitization(token string, config TokenConf
tokenLower := strings.ToLower(token)
for _, pattern := range suspiciousPatterns {
if strings.Contains(tokenLower, pattern) {
err := fmt.Errorf("CRITICAL: %s token contains suspicious pattern: %s", config.Type, pattern)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token contains suspicious pattern: %s", config.Type, pattern)
return err
}
}
@@ -535,8 +513,7 @@ func (cm *ChunkManager) validateTokenSanitization(token string, config TokenConf
func (cm *ChunkManager) validateJWTContent(token string, config TokenConfig) error {
parts := strings.Split(token, ".")
if len(parts) != 3 {
err := fmt.Errorf("CRITICAL: %s JWT token malformed for content validation", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s JWT token malformed for content validation", config.Type)
return err
}
@@ -562,15 +539,13 @@ func (cm *ChunkManager) validateJWTContent(token string, config TokenConfig) err
func (cm *ChunkManager) validateJWTHeader(header string, config TokenConfig) error {
// Basic header structure validation
if len(header) == 0 {
err := fmt.Errorf("CRITICAL: %s JWT header is empty", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s JWT header is empty", config.Type)
return err
}
// Validate base64url encoding
if _, err := base64.RawURLEncoding.DecodeString(header); err != nil {
err := fmt.Errorf("CRITICAL: %s JWT header not valid base64url", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s JWT header not valid base64url", config.Type)
return err
}
@@ -581,15 +556,13 @@ func (cm *ChunkManager) validateJWTHeader(header string, config TokenConfig) err
func (cm *ChunkManager) validateJWTPayload(payload string, config TokenConfig) error {
// Basic payload structure validation
if len(payload) == 0 {
err := fmt.Errorf("CRITICAL: %s JWT payload is empty", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s JWT payload is empty", config.Type)
return err
}
// Payload should be decodable (basic structural check)
if _, err := base64.RawURLEncoding.DecodeString(payload); err != nil {
err := fmt.Errorf("CRITICAL: %s JWT payload not valid base64url", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s JWT payload not valid base64url", config.Type)
return err
}
@@ -600,15 +573,13 @@ func (cm *ChunkManager) validateJWTPayload(payload string, config TokenConfig) e
func (cm *ChunkManager) validateJWTSignature(signature string, config TokenConfig) error {
// Basic signature structure validation
if len(signature) == 0 {
err := fmt.Errorf("CRITICAL: %s JWT signature is empty", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s JWT signature is empty", config.Type)
return err
}
// Validate base64url encoding
if _, err := base64.RawURLEncoding.DecodeString(signature); err != nil {
err := fmt.Errorf("CRITICAL: %s JWT signature not valid base64url", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s JWT signature not valid base64url", config.Type)
return err
}
@@ -641,8 +612,7 @@ func (cm *ChunkManager) validateOpaqueTokenContent(token string, config TokenCon
// Opaque tokens should have reasonable character distribution
if alphaRatio < 0.1 && numericRatio < 0.1 {
err := fmt.Errorf("CRITICAL: %s opaque token has suspicious character distribution", config.Type)
cm.logger.Error(err.Error())
err := fmt.Errorf("%s opaque token has suspicious character distribution", config.Type)
return err
}
}
@@ -665,7 +635,7 @@ func (cm *ChunkManager) validateOpaqueTokenContent(token string, config TokenCon
// For longer tokens without legitimate prefixes, be more suspicious
if len(token) > 50 && !hasLegitimatePrefix {
cm.logger.Debugf("WARNING: %s opaque token lacks common token prefixes", config.Type)
// Opaque token without common prefixes - this is fine
}
return nil
@@ -697,9 +667,8 @@ func (cm *ChunkManager) detectRepeatedCharacters(token string, config TokenConfi
// Flag tokens with excessive character repetition
threshold := 20 // Allow up to 20 consecutive identical characters
if maxRepeated > threshold {
err := fmt.Errorf("CRITICAL: %s token has excessive repeated characters (%d consecutive)",
err := fmt.Errorf("%s token has excessive repeated characters (%d consecutive)",
config.Type, maxRepeated)
cm.logger.Error(err.Error())
return err
}
@@ -715,9 +684,8 @@ func (cm *ChunkManager) detectRepeatedCharacters(token string, config TokenConfi
// Flag if any single character makes up more than 70% of the token
if frequency > 0.7 && tokenLen > 20 {
err := fmt.Errorf("CRITICAL: %s token has suspicious character frequency (char '%c': %.1f%%)",
err := fmt.Errorf("%s token has suspicious character frequency (char '%c': %.1f%%)",
config.Type, char, frequency*100)
cm.logger.Error(err.Error())
return err
}
}
@@ -742,8 +710,7 @@ func (cm *ChunkManager) validateTokenExpiration(token string, config TokenConfig
// Check if token is expired
if expiration != nil && time.Now().After(*expiration) {
err := fmt.Errorf("CRITICAL: %s token is expired (expired at: %v)", config.Type, expiration.Format(time.RFC3339))
cm.logger.Error(err.Error())
err := fmt.Errorf("%s token is expired (expired at: %v)", config.Type, expiration.Format(time.RFC3339))
return err
}
@@ -751,7 +718,7 @@ func (cm *ChunkManager) validateTokenExpiration(token string, config TokenConfig
if expiration != nil {
maxFutureTime := time.Now().Add(10 * 365 * 24 * time.Hour) // 10 years
if expiration.After(maxFutureTime) {
cm.logger.Infof("WARNING: %s token expires very far in future (%v) - potential security issue",
cm.logger.Info("%s token expires very far in future (%v) - potential security issue",
config.Type, expiration.Format(time.RFC3339))
}
}
@@ -819,16 +786,15 @@ func (cm *ChunkManager) validateTokenFreshness(token string, config TokenConfig)
// Check if token was issued in the future (clock skew tolerance: 5 minutes)
if issuedAt.After(now.Add(5 * time.Minute)) {
err := fmt.Errorf("CRITICAL: %s token issued in future (issued at: %v)",
err := fmt.Errorf("%s token issued in future (issued at: %v)",
config.Type, issuedAt.Format(time.RFC3339))
cm.logger.Error(err.Error())
return err
}
// Check if token is too old (potential replay attack)
maxAge := 24 * time.Hour // Tokens older than 24 hours are suspicious
if now.Sub(*issuedAt) > maxAge {
cm.logger.Infof("WARNING: %s token is quite old (issued: %v) - potential replay",
cm.logger.Info("%s token is quite old (issued: %v) - potential replay",
config.Type, issuedAt.Format(time.RFC3339))
}
}