mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +00:00
fixup! fixup! Simplify all the things.
This commit is contained in:
+8
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user