mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-11-03 07:44:13 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29ec26cbaa | ||
|
|
dc16b4fc94 |
+47
-23
@@ -9,12 +9,16 @@ import (
|
||||
// It provides a clean interface for starting and stopping periodic operations
|
||||
// with proper lifecycle management and logging.
|
||||
type BackgroundTask struct {
|
||||
stopChan chan struct{}
|
||||
taskFunc func()
|
||||
logger *Logger
|
||||
name string
|
||||
interval time.Duration
|
||||
wg *sync.WaitGroup
|
||||
stopChan chan struct{}
|
||||
taskFunc func()
|
||||
logger *Logger
|
||||
name string
|
||||
interval time.Duration
|
||||
externalWG *sync.WaitGroup // External WaitGroup (optional, for tracking by parent)
|
||||
internalWG sync.WaitGroup // Internal WaitGroup for this task's goroutine
|
||||
stopOnce sync.Once // Ensures Stop() can be called multiple times safely
|
||||
stopped bool // Track if task has been stopped
|
||||
mu sync.Mutex // Protects stopped flag
|
||||
}
|
||||
|
||||
// NewBackgroundTask creates a new background task with the specified parameters.
|
||||
@@ -29,37 +33,54 @@ type BackgroundTask struct {
|
||||
// Returns:
|
||||
// - A configured BackgroundTask ready to be started.
|
||||
func NewBackgroundTask(name string, interval time.Duration, taskFunc func(), logger *Logger, wg ...*sync.WaitGroup) *BackgroundTask {
|
||||
var waitGroup *sync.WaitGroup
|
||||
var externalWG *sync.WaitGroup
|
||||
if len(wg) > 0 {
|
||||
waitGroup = wg[0]
|
||||
externalWG = wg[0]
|
||||
}
|
||||
return &BackgroundTask{
|
||||
name: name,
|
||||
interval: interval,
|
||||
stopChan: make(chan struct{}),
|
||||
taskFunc: taskFunc,
|
||||
logger: logger,
|
||||
wg: waitGroup,
|
||||
name: name,
|
||||
interval: interval,
|
||||
stopChan: make(chan struct{}),
|
||||
taskFunc: taskFunc,
|
||||
logger: logger,
|
||||
externalWG: externalWG,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the background task execution in a separate goroutine.
|
||||
// The task runs immediately upon start and then at the specified interval.
|
||||
func (bt *BackgroundTask) Start() {
|
||||
if bt.wg != nil {
|
||||
bt.wg.Add(1)
|
||||
bt.mu.Lock()
|
||||
defer bt.mu.Unlock()
|
||||
|
||||
if bt.stopped {
|
||||
if bt.logger != nil {
|
||||
bt.logger.Infof("Attempted to start already stopped task: %s", bt.name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Add to both internal and external WaitGroups
|
||||
bt.internalWG.Add(1)
|
||||
if bt.externalWG != nil {
|
||||
bt.externalWG.Add(1)
|
||||
}
|
||||
go bt.run()
|
||||
}
|
||||
|
||||
// Stop gracefully terminates the background task by closing the stop channel.
|
||||
// If a WaitGroup was provided, it waits for the goroutine to complete.
|
||||
// It waits for the goroutine to complete using the internal WaitGroup.
|
||||
// This method is safe to call multiple times.
|
||||
func (bt *BackgroundTask) Stop() {
|
||||
close(bt.stopChan)
|
||||
if bt.wg != nil {
|
||||
bt.wg.Wait()
|
||||
}
|
||||
bt.stopOnce.Do(func() {
|
||||
bt.mu.Lock()
|
||||
bt.stopped = true
|
||||
bt.mu.Unlock()
|
||||
|
||||
close(bt.stopChan)
|
||||
// Wait only on the internal WaitGroup
|
||||
bt.internalWG.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// run is the main execution loop for the background task.
|
||||
@@ -67,8 +88,11 @@ func (bt *BackgroundTask) Stop() {
|
||||
// until the stop signal is received.
|
||||
func (bt *BackgroundTask) run() {
|
||||
defer func() {
|
||||
if bt.wg != nil {
|
||||
bt.wg.Done()
|
||||
// Always decrement internal WaitGroup
|
||||
bt.internalWG.Done()
|
||||
// Decrement external WaitGroup if provided
|
||||
if bt.externalWG != nil {
|
||||
bt.externalWG.Done()
|
||||
}
|
||||
}()
|
||||
ticker := time.NewTicker(bt.interval)
|
||||
|
||||
+9
-2
@@ -41,9 +41,16 @@ func (m *mockTraefikOidc) VerifyJWTSignatureAndClaims(jwt *JWT, token string) er
|
||||
}
|
||||
|
||||
func TestAzureOIDCRegression(t *testing.T) {
|
||||
// Create test cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create a mocked TraefikOidc instance configured for Azure AD
|
||||
mockLogger := NewLogger("debug")
|
||||
|
||||
// Create caches with cleanup tracking
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
|
||||
// Configure for Azure AD provider
|
||||
baseOidc := &TraefikOidc{
|
||||
issuerURL: "https://login.microsoftonline.com/tenant-id/v2.0",
|
||||
@@ -58,8 +65,8 @@ func TestAzureOIDCRegression(t *testing.T) {
|
||||
logger: mockLogger,
|
||||
httpClient: createDefaultHTTPClient(), // Add HTTP client
|
||||
jwkCache: &JWKCache{}, // Add JWK cache
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: tokenCache,
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
allowedUserDomains: make(map[string]struct{}),
|
||||
allowedUsers: make(map[string]struct{}),
|
||||
allowedRolesAndGroups: make(map[string]struct{}),
|
||||
|
||||
@@ -53,7 +53,7 @@ func NewCacheWithLogger(logger *Logger) *Cache {
|
||||
order: list.New(),
|
||||
elems: make(map[string]*list.Element, DefaultMaxSize),
|
||||
maxSize: DefaultMaxSize,
|
||||
autoCleanupInterval: 5 * time.Minute,
|
||||
autoCleanupInterval: 15 * time.Minute, // Increased from 5 minutes to reduce overhead
|
||||
logger: logger,
|
||||
}
|
||||
c.startAutoCleanup()
|
||||
@@ -155,16 +155,21 @@ func (c *Cache) Cleanup() {
|
||||
}
|
||||
|
||||
// evictOldest removes the least recently used (oldest) item from the cache.
|
||||
// It first attempts to find and remove an expired item from the front of the LRU list.
|
||||
// If no expired items are found at the front, it removes the absolute oldest item (front of the list).
|
||||
// It first attempts to find and remove expired items, checking up to 5 items
|
||||
// from the front of the LRU list for efficiency. If no expired items are found,
|
||||
// it removes the absolute oldest item (front of the list).
|
||||
// This method is called internally by Set when the cache reaches its maximum size.
|
||||
// Note: This function assumes the write lock is already held.
|
||||
func (c *Cache) evictOldest() {
|
||||
now := time.Now()
|
||||
elem := c.order.Front()
|
||||
|
||||
// First try to find an expired item from the front
|
||||
for elem != nil {
|
||||
// Check up to 5 items from the front for expired entries
|
||||
// This limits the search overhead while still finding expired items efficiently
|
||||
const maxExpiredCheck = 5
|
||||
checked := 0
|
||||
|
||||
for elem != nil && checked < maxExpiredCheck {
|
||||
entry := elem.Value.(lruEntry)
|
||||
if item, exists := c.items[entry.key]; exists {
|
||||
if now.After(item.ExpiresAt) {
|
||||
@@ -173,9 +178,10 @@ func (c *Cache) evictOldest() {
|
||||
}
|
||||
}
|
||||
elem = elem.Next()
|
||||
checked++
|
||||
}
|
||||
|
||||
// If no expired items found, remove the oldest item
|
||||
// If no expired items found in the first few entries, remove the oldest item
|
||||
if elem = c.order.Front(); elem != nil {
|
||||
entry := elem.Value.(lruEntry)
|
||||
c.removeItem(entry.key)
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestCacheMemoryLeaks tests various cache scenarios for memory leaks
|
||||
func TestCacheMemoryLeaks(t *testing.T) {
|
||||
t.Run("Cache doesn't release expired items memory", func(t *testing.T) {
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
cache := NewCache()
|
||||
defer cache.Close()
|
||||
|
||||
// Add many large items with short expiration
|
||||
largeData := make([]byte, 1024*1024) // 1MB
|
||||
for i := 0; i < 100; i++ {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
// Items expire in 1 second
|
||||
cache.Set(key, largeData, 1*time.Second)
|
||||
}
|
||||
|
||||
// Wait for items to expire
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Force cleanup
|
||||
cache.Cleanup()
|
||||
|
||||
// Check memory after cleanup
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
afterCleanupAlloc := m.Alloc
|
||||
|
||||
allocIncrease := float64(afterCleanupAlloc-baselineAlloc) / 1024 / 1024
|
||||
t.Logf("Memory after adding and expiring 100MB of data: %.2f MB", allocIncrease)
|
||||
|
||||
// Memory should be mostly released after cleanup
|
||||
if allocIncrease > 10.0 {
|
||||
t.Errorf("Cache retains too much memory after cleanup: %.2f MB", allocIncrease)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Token blacklist unbounded growth", func(t *testing.T) {
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
blacklist := NewCache()
|
||||
blacklist.SetMaxSize(1000) // Limit size
|
||||
defer blacklist.Close()
|
||||
|
||||
// Simulate continuous token blacklisting
|
||||
for i := 0; i < 10000; i++ {
|
||||
token := fmt.Sprintf("token-%d", i)
|
||||
// All tokens expire in 24 hours (typical blacklist duration)
|
||||
blacklist.Set(token, true, 24*time.Hour)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
currentAlloc := m.Alloc
|
||||
|
||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||
t.Logf("Memory after adding 10000 blacklisted tokens (max 1000): %.2f MB", allocIncrease)
|
||||
|
||||
// Should respect max size limit
|
||||
if len(blacklist.items) > 1000 {
|
||||
t.Errorf("Blacklist exceeded max size: %d items", len(blacklist.items))
|
||||
}
|
||||
|
||||
// Memory should be bounded
|
||||
if allocIncrease > 5.0 {
|
||||
t.Errorf("Blacklist uses too much memory: %.2f MB for max 1000 items", allocIncrease)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Replay cache with high JTI volume", func(t *testing.T) {
|
||||
initReplayCache()
|
||||
defer cleanupReplayCache()
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
// Simulate high volume of JTIs
|
||||
for i := 0; i < 20000; i++ {
|
||||
jti := fmt.Sprintf("jti-%d", i)
|
||||
replayCacheMu.Lock()
|
||||
if replayCache != nil {
|
||||
// JTIs expire after token expiry (typically 1 hour)
|
||||
replayCache.Set(jti, true, 1*time.Hour)
|
||||
}
|
||||
replayCacheMu.Unlock()
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
currentAlloc := m.Alloc
|
||||
|
||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||
t.Logf("Memory after adding 20000 JTIs (max 10000): %.2f MB", allocIncrease)
|
||||
|
||||
// Check size limit is enforced
|
||||
replayCacheMu.RLock()
|
||||
cacheSize := 0
|
||||
if replayCache != nil {
|
||||
cacheSize = len(replayCache.items)
|
||||
}
|
||||
replayCacheMu.RUnlock()
|
||||
|
||||
if cacheSize > 10000 {
|
||||
t.Errorf("Replay cache exceeded max size: %d items", cacheSize)
|
||||
}
|
||||
|
||||
// Memory should be bounded
|
||||
if allocIncrease > 10.0 {
|
||||
t.Errorf("Replay cache uses too much memory: %.2f MB for max 10000 items", allocIncrease)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cache cleanup interval effectiveness", func(t *testing.T) {
|
||||
// Create a cache with custom settings - don't use NewCache to avoid default cleanup
|
||||
cache := &Cache{
|
||||
items: make(map[string]CacheItem, DefaultMaxSize),
|
||||
order: list.New(),
|
||||
elems: make(map[string]*list.Element, DefaultMaxSize),
|
||||
maxSize: DefaultMaxSize,
|
||||
autoCleanupInterval: 200 * time.Millisecond, // Fast cleanup for test
|
||||
logger: newNoOpLogger(),
|
||||
}
|
||||
|
||||
// Start cleanup with our custom interval
|
||||
cache.startAutoCleanup()
|
||||
defer cache.Close()
|
||||
|
||||
// Add expired items
|
||||
for i := 0; i < 1000; i++ {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
cache.Set(key, "data", 50*time.Millisecond) // Very short expiry
|
||||
}
|
||||
|
||||
// Wait for items to expire and cleanup to run (at least 2 cleanup cycles)
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
|
||||
// Manually trigger cleanup to ensure it runs
|
||||
cache.Cleanup()
|
||||
|
||||
// Check that expired items are removed
|
||||
cache.mutex.RLock()
|
||||
remainingItems := len(cache.items)
|
||||
cache.mutex.RUnlock()
|
||||
|
||||
t.Logf("Remaining items after auto cleanup: %d", remainingItems)
|
||||
|
||||
if remainingItems > 100 {
|
||||
t.Errorf("Auto cleanup not effective: %d items remain", remainingItems)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Concurrent cache operations memory stability", func(t *testing.T) {
|
||||
cache := NewCache()
|
||||
defer cache.Close()
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
var wg sync.WaitGroup
|
||||
stop := make(chan struct{})
|
||||
|
||||
// Writers continuously add items
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 1000; j++ {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
key := fmt.Sprintf("writer-%d-%d", id, j)
|
||||
cache.Set(key, "data", 1*time.Second)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Readers continuously read items
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 1000; j++ {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
key := fmt.Sprintf("writer-%d-%d", id%5, j)
|
||||
cache.Get(key)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Let it run for a bit
|
||||
time.Sleep(5 * time.Second)
|
||||
close(stop)
|
||||
wg.Wait()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
finalAlloc := m.Alloc
|
||||
|
||||
// Handle potential underflow
|
||||
var allocIncrease float64
|
||||
if finalAlloc > baselineAlloc {
|
||||
allocIncrease = float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||
} else {
|
||||
allocIncrease = -float64(baselineAlloc-finalAlloc) / 1024 / 1024
|
||||
}
|
||||
t.Logf("Memory increase under concurrent load: %.2f MB", allocIncrease)
|
||||
|
||||
if allocIncrease > 5.0 {
|
||||
t.Errorf("Memory leak under concurrent operations: %.2f MB", allocIncrease)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LRU eviction memory release", func(t *testing.T) {
|
||||
cache := NewCache()
|
||||
cache.SetMaxSize(100) // Small cache
|
||||
defer cache.Close()
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
// Add many items to trigger eviction
|
||||
for i := 0; i < 1000; i++ {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
data := make([]byte, 10240) // 10KB per item
|
||||
cache.Set(key, data, 1*time.Hour)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
afterEvictionAlloc := m.Alloc
|
||||
|
||||
allocIncrease := float64(afterEvictionAlloc-baselineAlloc) / 1024 / 1024
|
||||
t.Logf("Memory after LRU eviction (1000 items, max 100): %.2f MB", allocIncrease)
|
||||
|
||||
// Should only keep 100 items worth of memory
|
||||
if allocIncrease > 2.0 { // 100 * 10KB = ~1MB
|
||||
t.Errorf("LRU eviction doesn't release memory properly: %.2f MB", allocIncrease)
|
||||
}
|
||||
|
||||
// Verify cache size
|
||||
if len(cache.items) > 100 {
|
||||
t.Errorf("Cache size exceeded limit: %d items", len(cache.items))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Token cache with claims memory", func(t *testing.T) {
|
||||
tokenCache := NewTokenCache()
|
||||
defer tokenCache.Close()
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
// Add tokens with large claims
|
||||
for i := 0; i < 1000; i++ {
|
||||
token := fmt.Sprintf("token-%d", i)
|
||||
claims := map[string]interface{}{
|
||||
"sub": fmt.Sprintf("user-%d", i),
|
||||
"email": fmt.Sprintf("user%[email protected]", i),
|
||||
"groups": make([]string, 100), // Large groups list
|
||||
"data": make([]byte, 1024), // Extra data
|
||||
}
|
||||
tokenCache.Set(token, claims, 1*time.Hour)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
currentAlloc := m.Alloc
|
||||
|
||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||
t.Logf("Memory after adding 1000 tokens with large claims: %.2f MB", allocIncrease)
|
||||
|
||||
// Check if memory is reasonable
|
||||
if allocIncrease > 20.0 {
|
||||
t.Errorf("Token cache uses excessive memory: %.2f MB", allocIncrease)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCacheEvictionBug tests the inefficient eviction in evictOldest
|
||||
func TestCacheEvictionBug(t *testing.T) {
|
||||
t.Run("evictOldest scans entire list", func(t *testing.T) {
|
||||
cache := NewCache()
|
||||
cache.SetMaxSize(100)
|
||||
defer cache.Close()
|
||||
|
||||
// Fill cache with non-expired items
|
||||
for i := 0; i < 100; i++ {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
cache.Set(key, "data", 1*time.Hour) // Long expiry
|
||||
}
|
||||
|
||||
// Try to add one more item to trigger eviction
|
||||
start := time.Now()
|
||||
cache.Set("trigger", "data", 1*time.Hour)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
t.Logf("Time to evict and add one item: %v", elapsed)
|
||||
|
||||
// Should be fast even with full cache
|
||||
if elapsed > 10*time.Millisecond {
|
||||
t.Errorf("Eviction too slow, possibly scanning entire list: %v", elapsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
+27
-28
@@ -7,45 +7,36 @@ import (
|
||||
|
||||
func TestCache_Cleanup(t *testing.T) {
|
||||
c := NewCache()
|
||||
defer c.Close() // Stop the auto-cleanup goroutine
|
||||
|
||||
// Add some items with different expiration times
|
||||
now := time.Now()
|
||||
pastTime := now.Add(-1 * time.Hour) // Already expired
|
||||
futureTime := now.Add(1 * time.Hour) // Not expired
|
||||
|
||||
// Create test items
|
||||
c.items["expired"] = CacheItem{
|
||||
Value: "expired-value",
|
||||
ExpiresAt: pastTime,
|
||||
}
|
||||
|
||||
c.items["valid"] = CacheItem{
|
||||
Value: "valid-value",
|
||||
ExpiresAt: futureTime,
|
||||
}
|
||||
|
||||
// Store original elements in the order list to match items
|
||||
c.elems["expired"] = c.order.PushBack(lruEntry{key: "expired"})
|
||||
c.elems["valid"] = c.order.PushBack(lruEntry{key: "valid"})
|
||||
// Use the Set method to properly add items with synchronization
|
||||
c.Set("expired", "expired-value", pastTime.Sub(now))
|
||||
c.Set("valid", "valid-value", futureTime.Sub(now))
|
||||
|
||||
// Call cleanup, which should only remove expired items
|
||||
c.Cleanup()
|
||||
|
||||
// Check that only the expired item was removed
|
||||
if _, exists := c.items["expired"]; exists {
|
||||
if _, exists := c.Get("expired"); exists {
|
||||
t.Error("Expired item was not removed by Cleanup()")
|
||||
}
|
||||
|
||||
if _, exists := c.items["valid"]; !exists {
|
||||
if _, exists := c.Get("valid"); !exists {
|
||||
t.Error("Valid item was incorrectly removed by Cleanup()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_SetMaxSize(t *testing.T) {
|
||||
c := NewCache()
|
||||
defer c.Close() // Stop the auto-cleanup goroutine
|
||||
|
||||
// Set a lower max size
|
||||
originalMaxSize := c.maxSize
|
||||
originalMaxSize := 10
|
||||
newMaxSize := 3
|
||||
|
||||
// Add more items than the new max size
|
||||
@@ -54,25 +45,33 @@ func TestCache_SetMaxSize(t *testing.T) {
|
||||
c.Set(key, i, 1*time.Hour)
|
||||
}
|
||||
|
||||
// Verify items were added
|
||||
if len(c.items) != originalMaxSize {
|
||||
t.Errorf("Expected %d items before SetMaxSize, got %d", originalMaxSize, len(c.items))
|
||||
// Verify items were added by checking a few keys
|
||||
for i := 0; i < originalMaxSize; i++ {
|
||||
key := "key" + string(rune('A'+i))
|
||||
if _, exists := c.Get(key); !exists {
|
||||
t.Errorf("Expected key %s to exist before SetMaxSize", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Change the max size to a smaller value
|
||||
c.SetMaxSize(newMaxSize)
|
||||
|
||||
// Count remaining items
|
||||
count := 0
|
||||
for i := 0; i < originalMaxSize; i++ {
|
||||
key := "key" + string(rune('A'+i))
|
||||
if _, exists := c.Get(key); exists {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the cache was reduced to the new max size
|
||||
if len(c.items) > newMaxSize {
|
||||
t.Errorf("Cache size %d exceeds new max size %d after SetMaxSize", len(c.items), newMaxSize)
|
||||
if count > newMaxSize {
|
||||
t.Errorf("Cache size %d exceeds new max size %d after SetMaxSize", count, newMaxSize)
|
||||
}
|
||||
|
||||
if c.maxSize != newMaxSize {
|
||||
t.Errorf("Cache maxSize not updated, expected %d, got %d", newMaxSize, c.maxSize)
|
||||
}
|
||||
|
||||
// Check that the oldest items were evicted (should keep "keyC", "keyD", "keyE", etc.)
|
||||
if _, exists := c.items["keyA"]; exists {
|
||||
// Check that the oldest items were evicted (keyA should be evicted)
|
||||
if _, exists := c.Get("keyA"); exists {
|
||||
t.Error("Expected oldest item 'keyA' to be evicted, but it still exists")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// TestBackgroundGoroutineLeaks tests that background goroutines don't leak memory
|
||||
// even when no requests are made to protected resources
|
||||
func TestBackgroundGoroutineLeaks(t *testing.T) {
|
||||
t.Run("Idle middleware memory growth", func(t *testing.T) {
|
||||
// Force GC to get clean baseline
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
baselineGoroutines := runtime.NumGoroutine()
|
||||
|
||||
t.Logf("Baseline: Memory=%d KB, Goroutines=%d", baselineAlloc/1024, baselineGoroutines)
|
||||
|
||||
// Create test cleanup
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create middleware instance with mock setup
|
||||
config := createTestConfig()
|
||||
config.LogLevel = "error" // Reduce noise
|
||||
|
||||
middleware, server := setupTestOIDCMiddleware(t, config)
|
||||
tc.addServer(server)
|
||||
tc.addOIDC(middleware)
|
||||
|
||||
// Let it sit idle for a while - simulating no requests
|
||||
// During this time, background goroutines are running
|
||||
t.Log("Letting middleware sit idle for 3 seconds...")
|
||||
|
||||
// Take measurements every 1 second
|
||||
for i := 0; i < 3; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
currentAlloc := m.Alloc
|
||||
currentGoroutines := runtime.NumGoroutine()
|
||||
|
||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||
goroutineIncrease := currentGoroutines - baselineGoroutines
|
||||
|
||||
t.Logf("After %d seconds: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||
(i + 1), allocIncrease, goroutineIncrease)
|
||||
|
||||
// Check for significant memory growth (more than 5MB)
|
||||
if allocIncrease > 5.0 {
|
||||
t.Errorf("Significant memory increase detected: %.2f MB after %d seconds of idle",
|
||||
allocIncrease, (i + 1))
|
||||
}
|
||||
|
||||
// Check for goroutine leaks (more than 10 extra goroutines)
|
||||
if goroutineIncrease > 10 {
|
||||
t.Errorf("Goroutine leak detected: %d extra goroutines after %d seconds",
|
||||
goroutineIncrease, (i+1)*5)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
if err := middleware.Close(); err != nil {
|
||||
t.Errorf("Failed to close middleware: %v", err)
|
||||
}
|
||||
|
||||
// Wait for cleanup
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Final check
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
finalAlloc := m.Alloc
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
|
||||
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||
finalGoroutineIncrease := finalGoroutines - baselineGoroutines
|
||||
|
||||
t.Logf("Final: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||
finalAllocIncrease, finalGoroutineIncrease)
|
||||
|
||||
if finalGoroutineIncrease > 2 {
|
||||
t.Errorf("Goroutines not cleaned up properly: %d extra goroutines remain",
|
||||
finalGoroutineIncrease)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestHTTPClientConnectionLeaks tests that HTTP clients don't leak connections
|
||||
func TestHTTPClientConnectionLeaks(t *testing.T) {
|
||||
t.Run("HTTP client connection accumulation", func(t *testing.T) {
|
||||
// Create test server that simulates OIDC endpoints
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/.well-known/openid-configuration":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{
|
||||
"issuer": "https://example.com",
|
||||
"authorization_endpoint": "https://example.com/auth",
|
||||
"token_endpoint": "https://example.com/token",
|
||||
"jwks_uri": "https://example.com/jwks",
|
||||
"userinfo_endpoint": "https://example.com/userinfo"
|
||||
}`))
|
||||
case "/jwks":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"keys": []}`))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Monitor connection count
|
||||
getActiveConnections := func(client *http.Client) int {
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
// This is a simplified check - in reality we'd need to inspect
|
||||
// the transport's connection pool
|
||||
return transport.MaxIdleConnsPerHost
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Create multiple HTTP clients like the middleware does
|
||||
clients := make([]*http.Client, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
clients[i] = createDefaultHTTPClient()
|
||||
|
||||
// Make requests
|
||||
resp, err := clients[i].Get(server.URL + "/.well-known/openid-configuration")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Check connection settings
|
||||
conns := getActiveConnections(clients[i])
|
||||
if conns > 1 {
|
||||
t.Logf("Client %d has %d max idle connections per host", i, conns)
|
||||
}
|
||||
}
|
||||
|
||||
// Let connections sit idle briefly to test cleanup
|
||||
time.Sleep(1 * time.Second) // Reduced for faster tests
|
||||
|
||||
// Force cleanup
|
||||
for _, client := range clients {
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
t.Log("HTTP clients cleaned up successfully")
|
||||
})
|
||||
}
|
||||
|
||||
// TestCacheBackgroundTaskLeaks tests that cache background tasks don't leak
|
||||
func TestCacheBackgroundTaskLeaks(t *testing.T) {
|
||||
t.Run("Multiple cache instances with cleanup tasks", func(t *testing.T) {
|
||||
// Reset global state to prevent test interference
|
||||
resetGlobalState()
|
||||
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
// Create many cache instances
|
||||
caches := make([]*Cache, 50)
|
||||
|
||||
// Ensure cleanup even on test failure or panic
|
||||
defer func() {
|
||||
for _, cache := range caches {
|
||||
if cache != nil {
|
||||
cache.Close()
|
||||
}
|
||||
}
|
||||
// Wait a bit for goroutines to stop
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}()
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
caches[i] = NewCache()
|
||||
// Add some data
|
||||
for j := 0; j < 100; j++ {
|
||||
key := fmt.Sprintf("key-%d-%d", i, j)
|
||||
caches[i].Set(key, "value", 5*time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all cleanup goroutines to start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
afterCreateGoroutines := runtime.NumGoroutine()
|
||||
goroutineIncrease := afterCreateGoroutines - initialGoroutines
|
||||
|
||||
t.Logf("Created %d caches, goroutine increase: %d", len(caches), goroutineIncrease)
|
||||
|
||||
// Expected: one cleanup goroutine per cache
|
||||
if goroutineIncrease < len(caches) {
|
||||
t.Errorf("Expected at least %d goroutines, got %d increase",
|
||||
len(caches), goroutineIncrease)
|
||||
}
|
||||
|
||||
// Close all caches
|
||||
for _, cache := range caches {
|
||||
cache.Close()
|
||||
}
|
||||
|
||||
// Wait for goroutines to stop with timeout
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for i := 0; i < 20; i++ { // Try for 2 seconds
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
currentGoroutines := runtime.NumGoroutine()
|
||||
if currentGoroutines-initialGoroutines <= 5 {
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
done <- false
|
||||
}()
|
||||
|
||||
success := <-done
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
remainingGoroutines := finalGoroutines - initialGoroutines
|
||||
|
||||
if !success && remainingGoroutines > 5 { // Allow small tolerance
|
||||
t.Errorf("Cache cleanup goroutines not stopped properly: %d extra goroutines remain",
|
||||
remainingGoroutines)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestGlobalSingletonMemoryGrowth tests that global singletons don't grow unbounded
|
||||
func TestGlobalSingletonMemoryGrowth(t *testing.T) {
|
||||
t.Run("Global cache manager memory growth", func(t *testing.T) {
|
||||
// Clean up any existing global state
|
||||
CleanupGlobalCacheManager()
|
||||
CleanupGlobalMemoryPools()
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
// Get global cache manager
|
||||
wg := &sync.WaitGroup{}
|
||||
cm := GetGlobalCacheManager(wg)
|
||||
|
||||
// Simulate continuous usage without cleanup
|
||||
for i := 0; i < 1000; i++ {
|
||||
// Add to token cache
|
||||
cm.GetSharedTokenCache().Set(
|
||||
fmt.Sprintf("token-%d", i),
|
||||
map[string]interface{}{"claim": fmt.Sprintf("value-%d", i)},
|
||||
5*time.Minute,
|
||||
)
|
||||
|
||||
// Add to blacklist
|
||||
cm.GetSharedTokenBlacklist().Set(
|
||||
fmt.Sprintf("blacklist-%d", i),
|
||||
true,
|
||||
5*time.Minute,
|
||||
)
|
||||
|
||||
// Every 100 iterations, check memory
|
||||
if i%100 == 0 {
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
currentAlloc := m.Alloc
|
||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||
|
||||
t.Logf("After %d items: Memory increase=%.2f MB", i, allocIncrease)
|
||||
|
||||
// The caches should have max size limits
|
||||
// If memory grows more than 10MB, there's likely a leak
|
||||
if allocIncrease > 10.0 {
|
||||
t.Errorf("Excessive memory growth in global caches: %.2f MB after %d items",
|
||||
allocIncrease, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
CleanupGlobalCacheManager()
|
||||
CleanupGlobalMemoryPools()
|
||||
wg.Wait()
|
||||
|
||||
// Force full cleanup of replay cache too
|
||||
cleanupReplayCache()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
finalAlloc := m.Alloc
|
||||
|
||||
// Handle potential negative differences (memory freed beyond baseline)
|
||||
var finalAllocIncrease float64
|
||||
if finalAlloc >= baselineAlloc {
|
||||
finalAllocIncrease = float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||
} else {
|
||||
// Memory decreased below baseline - this is good!
|
||||
finalAllocIncrease = -float64(baselineAlloc-finalAlloc) / 1024 / 1024
|
||||
}
|
||||
|
||||
t.Logf("Final memory increase after cleanup: %.2f MB", finalAllocIncrease)
|
||||
|
||||
if finalAllocIncrease > 2.0 {
|
||||
t.Errorf("Memory not properly released after cleanup: %.2f MB remains",
|
||||
finalAllocIncrease)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMetadataCacheRefreshLeak tests for memory leaks in metadata refresh
|
||||
func TestMetadataCacheRefreshLeak(t *testing.T) {
|
||||
t.Run("Metadata cache refresh memory leak", func(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
cache := NewMetadataCacheWithLogger(wg, NewLogger("error"))
|
||||
defer cache.Close()
|
||||
|
||||
// Mock HTTP client that returns metadata
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Return a large metadata response to amplify any leaks
|
||||
w.Write([]byte(`{
|
||||
"issuer": "https://example.com",
|
||||
"authorization_endpoint": "https://example.com/auth",
|
||||
"token_endpoint": "https://example.com/token",
|
||||
"jwks_uri": "https://example.com/jwks",
|
||||
"userinfo_endpoint": "https://example.com/userinfo",
|
||||
"extra_field_1": "` + strings.Repeat("x", 1000) + `",
|
||||
"extra_field_2": "` + strings.Repeat("y", 1000) + `"
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
// Simulate many metadata refreshes
|
||||
for i := 0; i < 100; i++ {
|
||||
_, err := cache.GetMetadata(server.URL, client, NewLogger("error"))
|
||||
if err != nil {
|
||||
t.Logf("Metadata fetch error (expected for test server): %v", err)
|
||||
}
|
||||
|
||||
// Force cache expiry to trigger refresh
|
||||
cache.mutex.Lock()
|
||||
cache.expiresAt = time.Now().Add(-1 * time.Hour)
|
||||
cache.mutex.Unlock()
|
||||
|
||||
if i%20 == 0 && i > 0 {
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
currentAlloc := m.Alloc
|
||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||
|
||||
t.Logf("After %d refreshes: Memory increase=%.2f MB", i, allocIncrease)
|
||||
|
||||
// Metadata cache should only store one copy
|
||||
if allocIncrease > 3.0 {
|
||||
t.Errorf("Metadata cache leak detected: %.2f MB after %d refreshes",
|
||||
allocIncrease, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache.Close()
|
||||
wg.Wait()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
finalAlloc := m.Alloc
|
||||
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||
|
||||
t.Logf("Final memory after metadata cache closure: %.2f MB", finalAllocIncrease)
|
||||
|
||||
if finalAllocIncrease > 1.0 {
|
||||
t.Errorf("Metadata cache not cleaned properly: %.2f MB remains", finalAllocIncrease)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMemoryPoolLeak tests for leaks in memory pool management
|
||||
func TestMemoryPoolLeak(t *testing.T) {
|
||||
t.Run("Memory pool buffer leaks", func(t *testing.T) {
|
||||
// Clean up any existing pools
|
||||
CleanupGlobalMemoryPools()
|
||||
|
||||
pools := GetGlobalMemoryPools()
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
|
||||
// Simulate heavy buffer usage
|
||||
var buffers [][]byte
|
||||
for i := 0; i < 1000; i++ {
|
||||
buf := pools.GetHTTPResponseBuffer()
|
||||
// Simulate using the buffer
|
||||
copy(buf, []byte("test data"))
|
||||
|
||||
// 90% of the time, return the buffer
|
||||
// 10% of the time, "forget" to return it (simulating a leak)
|
||||
if i%10 != 0 {
|
||||
pools.PutHTTPResponseBuffer(buf)
|
||||
} else {
|
||||
// Keep reference to simulate leak
|
||||
buffers = append(buffers, buf)
|
||||
}
|
||||
|
||||
if i%100 == 0 && i > 0 {
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
currentAlloc := m.Alloc
|
||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||
|
||||
t.Logf("After %d buffer operations: Memory increase=%.2f MB, Leaked buffers=%d",
|
||||
i, allocIncrease, len(buffers))
|
||||
|
||||
// With proper pooling, memory should be bounded
|
||||
if allocIncrease > 5.0 {
|
||||
t.Errorf("Memory pool leak detected: %.2f MB after %d operations",
|
||||
allocIncrease, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the "leaked" buffers
|
||||
for _, buf := range buffers {
|
||||
pools.PutHTTPResponseBuffer(buf)
|
||||
}
|
||||
|
||||
CleanupGlobalMemoryPools()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
finalAlloc := m.Alloc
|
||||
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||
|
||||
t.Logf("Final memory after pool cleanup: %.2f MB", finalAllocIncrease)
|
||||
|
||||
if finalAllocIncrease > 1.0 {
|
||||
t.Errorf("Memory pools not cleaned properly: %.2f MB remains", finalAllocIncrease)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcurrentMemoryLeaks tests for memory leaks under concurrent load
|
||||
func TestConcurrentMemoryLeaks(t *testing.T) {
|
||||
t.Run("Concurrent operations memory stability", func(t *testing.T) {
|
||||
// Reset global state
|
||||
resetGlobalState()
|
||||
|
||||
// Set lower GC percentage to trigger GC more frequently
|
||||
debug.SetGCPercent(50)
|
||||
defer debug.SetGCPercent(100)
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
baselineAlloc := m.Alloc
|
||||
baselineGoroutines := runtime.NumGoroutine()
|
||||
|
||||
// Create a mock OIDC server
|
||||
var mockServerURL string
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/.well-known/openid-configuration":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"issuer": mockServerURL,
|
||||
"authorization_endpoint": mockServerURL + "/auth",
|
||||
"token_endpoint": mockServerURL + "/token",
|
||||
"userinfo_endpoint": mockServerURL + "/userinfo",
|
||||
"jwks_uri": mockServerURL + "/keys",
|
||||
})
|
||||
case "/keys":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"keys": []map[string]interface{}{
|
||||
{
|
||||
"kty": "RSA",
|
||||
"use": "sig",
|
||||
"kid": "test-key",
|
||||
"n": "test-modulus",
|
||||
"e": "AQAB",
|
||||
},
|
||||
},
|
||||
})
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
mockServerURL = mockServer.URL
|
||||
|
||||
// Create middleware config with mock server
|
||||
config := createTestConfig()
|
||||
config.ProviderURL = mockServerURL
|
||||
config.LogLevel = "error"
|
||||
|
||||
// Create middleware directly without using New() to avoid automatic metadata fetch
|
||||
middleware := &TraefikOidc{
|
||||
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }),
|
||||
providerURL: config.ProviderURL,
|
||||
clientID: config.ClientID,
|
||||
clientSecret: config.ClientSecret,
|
||||
redirURLPath: "/callback",
|
||||
scopes: []string{"openid", "email", "profile"},
|
||||
logger: NewLogger(config.LogLevel),
|
||||
excludedURLs: make(map[string]struct{}),
|
||||
httpClient: &http.Client{Timeout: 5 * time.Second},
|
||||
sessionManager: nil, // Will be created below
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: NewCache(),
|
||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||
goroutineWG: &sync.WaitGroup{},
|
||||
firstRequestMutex: sync.Mutex{},
|
||||
firstRequestReceived: false,
|
||||
}
|
||||
|
||||
// Create session manager
|
||||
var err error
|
||||
middleware.sessionManager, err = NewSessionManager(
|
||||
config.SessionEncryptionKey,
|
||||
config.ForceHTTPS,
|
||||
config.CookieDomain,
|
||||
middleware.logger,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Simulate concurrent operations
|
||||
var wg sync.WaitGroup
|
||||
stopChan := make(chan struct{})
|
||||
|
||||
// Worker that continuously performs operations
|
||||
worker := func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
return
|
||||
default:
|
||||
// Simulate various operations
|
||||
switch id % 4 {
|
||||
case 0:
|
||||
// Cache operations
|
||||
cache := NewCache()
|
||||
cache.Set(fmt.Sprintf("key-%d", id), "value", time.Minute)
|
||||
cache.Get(fmt.Sprintf("key-%d", id))
|
||||
cache.Close()
|
||||
case 1:
|
||||
// Session operations
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
session, _ := middleware.sessionManager.GetSession(req)
|
||||
if session != nil {
|
||||
session.SetAccessToken("token")
|
||||
session.returnToPoolSafely()
|
||||
}
|
||||
case 2:
|
||||
// Token cache operations
|
||||
cm := GetGlobalCacheManager(nil)
|
||||
if cm != nil {
|
||||
tc := cm.GetSharedTokenCache()
|
||||
tc.Set("test-token", map[string]interface{}{"test": "data"}, time.Minute)
|
||||
tc.Get("test-token")
|
||||
}
|
||||
case 3:
|
||||
// Memory pool operations
|
||||
pools := GetGlobalMemoryPools()
|
||||
buf := pools.GetHTTPResponseBuffer()
|
||||
pools.PutHTTPResponseBuffer(buf)
|
||||
}
|
||||
|
||||
// Small delay to prevent tight loop
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start workers
|
||||
numWorkers := 20
|
||||
wg.Add(numWorkers)
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
go worker(i)
|
||||
}
|
||||
|
||||
// Let it run and measure periodically
|
||||
for i := 0; i < 3; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
currentAlloc := m.Alloc
|
||||
currentGoroutines := runtime.NumGoroutine()
|
||||
|
||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
||||
goroutineIncrease := currentGoroutines - baselineGoroutines
|
||||
|
||||
t.Logf("After %d seconds of concurrent load: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||
(i + 1), allocIncrease, goroutineIncrease)
|
||||
|
||||
// Under sustained concurrent load, memory should stabilize
|
||||
if i > 1 && allocIncrease > 20.0 {
|
||||
t.Errorf("Memory leak under concurrent load: %.2f MB after %d seconds",
|
||||
allocIncrease, (i + 1))
|
||||
}
|
||||
}
|
||||
|
||||
// Stop workers
|
||||
close(stopChan)
|
||||
wg.Wait()
|
||||
|
||||
// Clean up
|
||||
middleware.Close()
|
||||
|
||||
// Final measurements
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
finalAlloc := m.Alloc
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
|
||||
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
||||
finalGoroutineIncrease := finalGoroutines - baselineGoroutines
|
||||
|
||||
t.Logf("Final after cleanup: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||
finalAllocIncrease, finalGoroutineIncrease)
|
||||
|
||||
if finalGoroutineIncrease > 5 {
|
||||
t.Errorf("Goroutines not cleaned up after concurrent operations: %d extra remain",
|
||||
finalGoroutineIncrease)
|
||||
}
|
||||
|
||||
if finalAllocIncrease > 5.0 {
|
||||
t.Errorf("Memory not released after concurrent operations: %.2f MB remains",
|
||||
finalAllocIncrease)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -349,6 +349,9 @@ func TestGoogleOIDCRefreshTokenHandling(t *testing.T) {
|
||||
Err: nil,
|
||||
}
|
||||
|
||||
// Create test cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create a complete test instance with all required fields
|
||||
mockLogger := NewLogger("debug")
|
||||
googleTOidc := &TraefikOidc{
|
||||
@@ -358,8 +361,8 @@ func TestGoogleOIDCRefreshTokenHandling(t *testing.T) {
|
||||
logger: mockLogger,
|
||||
scopes: []string{"openid", "profile", "email"},
|
||||
refreshGracePeriod: 60,
|
||||
tokenCache: NewTokenCache(), // Initialize tokenCache
|
||||
tokenBlacklist: NewCache(), // Initialize tokenBlacklist
|
||||
tokenCache: tc.addTokenCache(NewTokenCache()), // Initialize tokenCache with cleanup
|
||||
tokenBlacklist: tc.addCache(NewCache()), // Initialize tokenBlacklist with cleanup
|
||||
enablePKCE: false,
|
||||
limiter: rate.NewLimiter(rate.Inf, 0), // No rate limiting for tests
|
||||
jwkCache: mockJWKCache,
|
||||
|
||||
@@ -17,9 +17,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
replayCacheMu sync.RWMutex // Use RWMutex for better read performance
|
||||
replayCache *Cache // Replace unbounded map with bounded Cache
|
||||
replayCacheOnce sync.Once
|
||||
replayCacheMu sync.RWMutex // Use RWMutex for better read performance
|
||||
replayCache *Cache // Replace unbounded map with bounded Cache
|
||||
replayCacheOnce sync.Once
|
||||
replayCacheCleanupWG sync.WaitGroup
|
||||
replayCacheCtx context.Context
|
||||
replayCacheCancel context.CancelFunc
|
||||
replayCacheCleanupMu sync.Mutex
|
||||
)
|
||||
|
||||
// initReplayCache initializes the global replay cache for JWT ID tracking.
|
||||
@@ -36,6 +40,18 @@ func initReplayCache() {
|
||||
// It acquires a write lock, closes the cache, and sets it to nil
|
||||
// to ensure proper cleanup during shutdown.
|
||||
func cleanupReplayCache() {
|
||||
// Cancel cleanup goroutine first
|
||||
replayCacheCleanupMu.Lock()
|
||||
if replayCacheCancel != nil {
|
||||
replayCacheCancel()
|
||||
replayCacheCancel = nil
|
||||
replayCacheCtx = nil
|
||||
}
|
||||
replayCacheCleanupMu.Unlock()
|
||||
|
||||
// Wait for cleanup goroutine to finish
|
||||
replayCacheCleanupWG.Wait()
|
||||
|
||||
replayCacheMu.Lock()
|
||||
defer replayCacheMu.Unlock()
|
||||
|
||||
@@ -73,7 +89,21 @@ func getReplayCacheStats() (size int, maxSize int) {
|
||||
// - ctx: Context for cancellation.
|
||||
// - logger: Logger for debug output (can be nil).
|
||||
func startReplayCacheCleanup(ctx context.Context, logger *Logger) {
|
||||
replayCacheCleanupMu.Lock()
|
||||
defer replayCacheCleanupMu.Unlock()
|
||||
|
||||
// Cancel any existing cleanup goroutine
|
||||
if replayCacheCancel != nil {
|
||||
replayCacheCancel()
|
||||
replayCacheCleanupWG.Wait()
|
||||
}
|
||||
|
||||
// Create new context for this cleanup goroutine
|
||||
replayCacheCtx, replayCacheCancel = context.WithCancel(ctx)
|
||||
|
||||
replayCacheCleanupWG.Add(1)
|
||||
go func() {
|
||||
defer replayCacheCleanupWG.Done()
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -91,8 +121,7 @@ func startReplayCacheCleanup(ctx context.Context, logger *Logger) {
|
||||
}
|
||||
replayCacheMu.RUnlock()
|
||||
|
||||
case <-ctx.Done():
|
||||
cleanupReplayCache()
|
||||
case <-replayCacheCtx.Done():
|
||||
if logger != nil {
|
||||
logger.Debug("Replay cache cleanup goroutine stopped due to context cancellation")
|
||||
}
|
||||
@@ -286,19 +315,17 @@ func (j *JWT) Verify(issuerURL, clientID string, skipReplayCheck ...bool) error
|
||||
|
||||
shouldSkipReplay := len(skipReplayCheck) > 0 && skipReplayCheck[0]
|
||||
|
||||
if jti, ok := claims["jti"].(string); ok && !shouldSkipReplay {
|
||||
if j.Token == "" {
|
||||
return nil
|
||||
}
|
||||
jtiValue, jtiOk := claims["jti"].(string)
|
||||
|
||||
if jtiOk && !shouldSkipReplay && jtiValue != "" {
|
||||
initReplayCache()
|
||||
|
||||
replayCacheMu.RLock()
|
||||
_, exists := replayCache.Get(jti)
|
||||
_, exists := replayCache.Get(jtiValue)
|
||||
replayCacheMu.RUnlock()
|
||||
|
||||
if exists {
|
||||
return fmt.Errorf("token replay detected (jti: %s)", jti)
|
||||
return fmt.Errorf("token replay detected (jti: %s)", jtiValue)
|
||||
}
|
||||
|
||||
expFloat, ok := claims["exp"].(float64)
|
||||
@@ -313,7 +340,7 @@ func (j *JWT) Verify(issuerURL, clientID string, skipReplayCheck ...bool) error
|
||||
if duration > 0 {
|
||||
replayCacheMu.Lock()
|
||||
if replayCache != nil {
|
||||
replayCache.Set(jti, true, duration)
|
||||
replayCache.Set(jtiValue, true, duration)
|
||||
}
|
||||
replayCacheMu.Unlock()
|
||||
}
|
||||
|
||||
@@ -42,17 +42,17 @@ func createDefaultHTTPClient() *http.Client {
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
},
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: 2 * time.Second, // Reduced TLS handshake timeout
|
||||
ExpectContinueTimeout: 1 * time.Second, // Timeout for 100-continue responses
|
||||
MaxIdleConns: 5, // Further reduced to prevent connection accumulation
|
||||
MaxIdleConnsPerHost: 1, // Further reduced to limit per-host connections
|
||||
IdleConnTimeout: 15 * time.Second, // Further reduced to close idle connections faster
|
||||
DisableKeepAlives: false, // Enable connection reuse
|
||||
MaxConnsPerHost: 5, // Further reduced to limit concurrent connections
|
||||
ResponseHeaderTimeout: 3 * time.Second, // Reduced timeout for reading response headers
|
||||
DisableCompression: false, // Enable compression for bandwidth efficiency
|
||||
WriteBufferSize: 4096, // Write buffer size for connections
|
||||
ReadBufferSize: 4096, // Read buffer size for connections
|
||||
TLSHandshakeTimeout: 2 * time.Second, // Reduced TLS handshake timeout
|
||||
ExpectContinueTimeout: 1 * time.Second, // Timeout for 100-continue responses
|
||||
MaxIdleConns: 2, // Reduced from 5 to minimize idle connections
|
||||
MaxIdleConnsPerHost: 1, // Keep minimal idle connections per host
|
||||
IdleConnTimeout: 5 * time.Second, // Reduced from 15s to close idle connections faster
|
||||
DisableKeepAlives: false, // Enable connection reuse
|
||||
MaxConnsPerHost: 2, // Reduced from 5 to limit concurrent connections
|
||||
ResponseHeaderTimeout: 3 * time.Second, // Reduced timeout for reading response headers
|
||||
DisableCompression: false, // Enable compression for bandwidth efficiency
|
||||
WriteBufferSize: 4096, // Write buffer size for connections
|
||||
ReadBufferSize: 4096, // Read buffer size for connections
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
@@ -290,6 +290,10 @@ type TraefikOidc struct {
|
||||
enablePKCE bool
|
||||
overrideScopes bool
|
||||
suppressDiagnosticLogs bool
|
||||
firstRequestReceived bool
|
||||
firstRequestMutex sync.Mutex
|
||||
metadataRefreshStarted bool
|
||||
providerURL string
|
||||
}
|
||||
|
||||
// ProviderMetadata represents the OpenID Connect provider's discovery metadata.
|
||||
@@ -773,7 +777,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||
|
||||
t.tokenVerifier = t
|
||||
t.jwtVerifier = t
|
||||
t.startTokenCleanup()
|
||||
// Delay startTokenCleanup() until first request
|
||||
t.tokenExchanger = t // Initialize the interface field to self
|
||||
|
||||
// Initialize and parse header templates with safe field access
|
||||
@@ -817,6 +821,9 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||
|
||||
startReplayCacheCleanup(pluginCtx, logger)
|
||||
logger.Debugf("TraefikOidc.New: Final t.scopes initialized to: %v", t.scopes)
|
||||
|
||||
// Store provider URL for later use
|
||||
t.providerURL = config.ProviderURL
|
||||
go t.initializeMetadata(config.ProviderURL)
|
||||
|
||||
return t, nil
|
||||
@@ -853,8 +860,8 @@ func (t *TraefikOidc) initializeMetadata(providerURL string) {
|
||||
t.logger.Debug("Successfully initialized provider metadata")
|
||||
t.updateMetadataEndpoints(metadata)
|
||||
|
||||
// Start metadata refresh goroutine
|
||||
go t.startMetadataRefresh(providerURL)
|
||||
// Delay metadata refresh goroutine until first request
|
||||
// It will be started in ServeHTTP when needed
|
||||
|
||||
// Only close channel on success
|
||||
close(t.initComplete)
|
||||
@@ -890,11 +897,19 @@ func (t *TraefikOidc) updateMetadataEndpoints(metadata *ProviderMetadata) {
|
||||
func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
||||
// Use longer interval to reduce memory pressure from refresh attempts
|
||||
ticker := time.NewTicker(2 * time.Hour) // Increased from 1 hour
|
||||
t.goroutineWG.Add(1) // Track this goroutine
|
||||
|
||||
// Check if WaitGroup is initialized (it might be nil in tests)
|
||||
if t.goroutineWG != nil {
|
||||
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() {
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||
}
|
||||
}()
|
||||
defer ticker.Stop() // Ensure ticker is always stopped
|
||||
|
||||
consecutiveFailures := 0
|
||||
for {
|
||||
@@ -1058,6 +1073,23 @@ func fetchMetadata(wellKnownURL string, httpClient *http.Client) (*ProviderMetad
|
||||
// - Authentication state management
|
||||
// - Header injection for authenticated requests
|
||||
func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// Start background tasks on first request (except for health checks)
|
||||
if !strings.HasPrefix(req.URL.Path, "/health") {
|
||||
t.firstRequestMutex.Lock()
|
||||
if !t.firstRequestReceived {
|
||||
t.firstRequestReceived = true
|
||||
t.logger.Debug("Starting background tasks on first request")
|
||||
t.startTokenCleanup()
|
||||
|
||||
// Also start metadata refresh if not already started
|
||||
if !t.metadataRefreshStarted && t.providerURL != "" {
|
||||
t.metadataRefreshStarted = true
|
||||
go t.startMetadataRefresh(t.providerURL)
|
||||
}
|
||||
}
|
||||
t.firstRequestMutex.Unlock()
|
||||
}
|
||||
|
||||
// Wait for provider metadata initialization to complete
|
||||
select {
|
||||
case <-t.initComplete:
|
||||
@@ -2050,11 +2082,17 @@ func (t *TraefikOidc) validateHost(host string) error {
|
||||
// panic recovery to ensure stability.
|
||||
func (t *TraefikOidc) startTokenCleanup() {
|
||||
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
|
||||
t.goroutineWG.Add(1) // Track this goroutine
|
||||
|
||||
// Check if WaitGroup is initialized (it might be nil in tests)
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Add(1) // Track this goroutine
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||
ticker.Stop() // Ensure ticker is always stopped
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||
}
|
||||
ticker.Stop() // Ensure ticker is always stopped
|
||||
|
||||
// CRITICAL: Recover from panics to prevent middleware crashes
|
||||
if r := recover(); r != nil {
|
||||
@@ -2840,17 +2878,22 @@ func (t *TraefikOidc) Close() error {
|
||||
t.logger.Debug("metadataRefreshStopChan closed")
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
t.goroutineWG.Wait()
|
||||
close(done)
|
||||
}()
|
||||
// Only wait for goroutines if WaitGroup is initialized
|
||||
if t.goroutineWG != nil {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
t.goroutineWG.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
t.logger.Debug("All background goroutines stopped gracefully")
|
||||
case <-time.After(10 * time.Second):
|
||||
t.logger.Errorf("Timeout waiting for background goroutines to stop")
|
||||
select {
|
||||
case <-done:
|
||||
t.logger.Debug("All background goroutines stopped gracefully")
|
||||
case <-time.After(10 * time.Second):
|
||||
t.logger.Errorf("Timeout waiting for background goroutines to stop")
|
||||
}
|
||||
} else {
|
||||
t.logger.Debug("No goroutineWG to wait for (likely in test)")
|
||||
}
|
||||
|
||||
if t.httpClient != nil {
|
||||
|
||||
+3
-1
@@ -10,7 +10,9 @@ import (
|
||||
func BenchmarkOIDCMiddleware(b *testing.B) {
|
||||
// Setup test environment
|
||||
|
||||
ts := &TestSuite{}
|
||||
// Create a testing.T wrapper for benchmarks
|
||||
t := &testing.T{}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
ts.token = "valid.jwt.token"
|
||||
|
||||
|
||||
+95
-95
@@ -32,10 +32,25 @@ type TestSuite struct {
|
||||
mockJWKCache *MockJWKCache
|
||||
sessionManager *SessionManager
|
||||
token string
|
||||
utf *UnifiedTestFramework // Add unified test framework
|
||||
}
|
||||
|
||||
// NewTestSuite creates a new test suite with automatic cleanup
|
||||
func NewTestSuite(t *testing.T) *TestSuite {
|
||||
ts := &TestSuite{
|
||||
t: t,
|
||||
utf: NewUnifiedTestFramework(t),
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
// Setup initializes the test suite
|
||||
func (ts *TestSuite) Setup() {
|
||||
// Initialize unified test framework if not already done
|
||||
if ts.utf == nil {
|
||||
ts.utf = NewUnifiedTestFramework(ts.t)
|
||||
}
|
||||
|
||||
var err error
|
||||
ts.rsaPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
@@ -92,6 +107,9 @@ func (ts *TestSuite) Setup() {
|
||||
logger := NewLogger("info")
|
||||
ts.sessionManager, _ = NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger)
|
||||
|
||||
// Create WaitGroup for the OIDC instance
|
||||
goroutineWG := ts.utf.NewWaitGroup()
|
||||
|
||||
// Common TraefikOidc instance
|
||||
ts.tOidc = &TraefikOidc{
|
||||
issuerURL: "https://test-issuer.com",
|
||||
@@ -101,19 +119,23 @@ func (ts *TestSuite) Setup() {
|
||||
jwksURL: "https://test-jwks-url.com",
|
||||
revocationURL: "https://revocation-endpoint.com",
|
||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||
tokenBlacklist: NewCache(), // Use generic cache for blacklist
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: ts.utf.NewCache(), // Use framework's tracked cache
|
||||
tokenCache: ts.utf.NewTokenCache(), // Use framework's tracked token cache
|
||||
logger: logger,
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
excludedURLs: map[string]struct{}{"/favicon": {}},
|
||||
httpClient: &http.Client{},
|
||||
httpClient: ts.utf.NewHTTPClient(), // Use framework's tracked HTTP client
|
||||
// Explicitly set paths as New() is bypassed
|
||||
redirURLPath: "/callback", // Assume default callback path for tests
|
||||
logoutURLPath: "/callback/logout", // Assume default logout path for tests
|
||||
tokenURL: "https://test-issuer.com/token", // Explicitly set for refresh tests
|
||||
extractClaimsFunc: extractClaims,
|
||||
initComplete: make(chan struct{}),
|
||||
sessionManager: ts.sessionManager,
|
||||
redirURLPath: "/callback", // Assume default callback path for tests
|
||||
logoutURLPath: "/callback/logout", // Assume default logout path for tests
|
||||
tokenURL: "https://test-issuer.com/token", // Explicitly set for refresh tests
|
||||
extractClaimsFunc: extractClaims,
|
||||
initComplete: make(chan struct{}),
|
||||
sessionManager: ts.sessionManager,
|
||||
goroutineWG: goroutineWG,
|
||||
ctx: ts.utf.NewContext(), // Use framework's tracked context
|
||||
tokenCleanupStopChan: make(chan struct{}),
|
||||
metadataRefreshStopChan: make(chan struct{}),
|
||||
}
|
||||
close(ts.tOidc.initComplete)
|
||||
// ts.tOidc.exchangeCodeForTokenFunc = ts.exchangeCodeForTokenFunc // Removed
|
||||
@@ -139,6 +161,9 @@ func (ts *TestSuite) Setup() {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Track the OIDC instance for cleanup
|
||||
ts.utf.AddOIDCInstance(ts.tOidc)
|
||||
}
|
||||
|
||||
// Helper function exchangeCodeForTokenFunc removed as it's unused after refactoring to TokenExchanger interface.
|
||||
@@ -272,7 +297,7 @@ func bigIntToBytes(i *big.Int) []byte {
|
||||
|
||||
// TestVerifyToken tests the VerifyToken method
|
||||
func TestVerifyToken(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -361,7 +386,7 @@ func TestVerifyToken(t *testing.T) {
|
||||
|
||||
// TestServeHTTP tests the ServeHTTP method
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -739,10 +764,8 @@ func TestServeHTTP(t *testing.T) {
|
||||
ts.tOidc.tokenCache = NewTokenCache()
|
||||
|
||||
// Reset the global replayCache to prevent "token replay detected" errors
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Store original tokenVerifier to restore later
|
||||
origTokenVerifier := ts.tOidc.tokenVerifier
|
||||
@@ -752,10 +775,8 @@ func TestServeHTTP(t *testing.T) {
|
||||
mockTokenVerifier := &MockTokenVerifier{
|
||||
VerifyFunc: func(token string) error {
|
||||
// Clear replay cache before token verification
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Call the original verifier's VerifyToken method
|
||||
// Ensure origTokenVerifier is not nil and is the correct type if necessary,
|
||||
@@ -869,7 +890,7 @@ func TestServeHTTP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJWKToPEM(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -922,7 +943,7 @@ func TestJWKToPEM(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseJWT(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -963,7 +984,7 @@ func TestParseJWT(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJWTVerify_MissingClaims(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
jwt := &JWT{
|
||||
@@ -983,7 +1004,7 @@ func TestJWTVerify_MissingClaims(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHandleCallback(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
redirectURL := "http://example.com/"
|
||||
@@ -1162,10 +1183,8 @@ func TestHandleCallback(t *testing.T) {
|
||||
// Capture range variable
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Clear the global replay cache before each test run
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Explicitly clear the shared blacklist at the start of each sub-test
|
||||
// to ensure no state leaks, even though we expect the local one to be used.
|
||||
@@ -1252,7 +1271,7 @@ func TestHandleCallback(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsAllowedDomain(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -1337,7 +1356,7 @@ func TestIsAllowedDomain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOIDCHandler(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
ts.token = "valid.jwt.token"
|
||||
@@ -1481,7 +1500,7 @@ func TestOIDCHandler(t *testing.T) {
|
||||
|
||||
// TestHandleLogout tests the logout functionality
|
||||
func TestHandleLogout(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create mock revocation endpoint server
|
||||
@@ -1652,7 +1671,7 @@ func TestHandleLogout(t *testing.T) {
|
||||
|
||||
// TestRevokeTokenWithProvider tests the token revocation with provider
|
||||
func TestRevokeTokenWithProvider(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -1728,7 +1747,7 @@ func TestRevokeTokenWithProvider(t *testing.T) {
|
||||
|
||||
// TestRevokeToken tests the token revocation functionality
|
||||
func TestRevokeToken(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
token := "test.token.with.claims"
|
||||
@@ -1820,7 +1839,7 @@ func TestBuildLogoutURL(t *testing.T) {
|
||||
|
||||
// Add this new test function
|
||||
func TestHandleExpiredToken(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -1928,7 +1947,7 @@ func TestHandleExpiredToken(t *testing.T) {
|
||||
|
||||
// Add this new test function
|
||||
func TestExtractGroupsAndRoles(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -2094,7 +2113,7 @@ func TestMultipleMiddlewareInstances(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServeHTTPRolesAndGroups(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create consistent timestamps for all test cases
|
||||
@@ -2309,7 +2328,7 @@ func stringSliceEqual(a, b []string) bool {
|
||||
|
||||
// TestExchangeTokensWithRedirects tests the token exchange process with redirects
|
||||
func TestExchangeTokensWithRedirects(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -2414,7 +2433,7 @@ func TestExchangeTokensWithRedirects(t *testing.T) {
|
||||
|
||||
// TestBuildAuthURL tests the buildAuthURL function with various URL scenarios
|
||||
func TestBuildAuthURL(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -2570,7 +2589,7 @@ func TestBuildAuthURL(t *testing.T) {
|
||||
|
||||
// TestExchangeCodeForToken tests the exchangeCodeForToken function with PKCE support
|
||||
func TestExchangeCodeForToken(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -2688,7 +2707,7 @@ func TestExchangeCodeForToken(t *testing.T) {
|
||||
|
||||
// TestDefaultInitiateAuthentication_PreservesQueryParameters tests that defaultInitiateAuthentication preserves query parameters in the incoming path.
|
||||
func TestDefaultInitiateAuthentication_PreservesQueryParameters(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a request with query parameters
|
||||
@@ -2845,14 +2864,12 @@ func TestVerifyTimeConstraint(t *testing.T) {
|
||||
|
||||
// TestJWTVerifyWithSkipReplayCheck tests the new skipReplayCheck parameter functionality
|
||||
func TestJWTVerifyWithSkipReplayCheck(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Clear the global replay cache before test
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Create a test JWT with unique JTI
|
||||
jti := generateRandomString(16)
|
||||
@@ -2913,10 +2930,8 @@ func TestJWTVerifyWithSkipReplayCheck(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.firstCall {
|
||||
// Clear replay cache for first call tests
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
}
|
||||
|
||||
err := jwt.Verify("https://test-issuer.com", "test-client-id", tc.skipReplayCheck)
|
||||
@@ -2938,14 +2953,12 @@ func TestJWTVerifyWithSkipReplayCheck(t *testing.T) {
|
||||
|
||||
// TestJWTVerifyBackwardCompatibility tests that calls without the skipReplayCheck parameter default to replay checking
|
||||
func TestJWTVerifyBackwardCompatibility(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Clear the global replay cache
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Create a test JWT with unique JTI
|
||||
jti := generateRandomString(16)
|
||||
@@ -2991,14 +3004,12 @@ func TestJWTVerifyBackwardCompatibility(t *testing.T) {
|
||||
|
||||
// TestTokenReplayDetectionFalsePositiveFix tests the specific scenario that was causing false positives
|
||||
func TestTokenReplayDetectionFalsePositiveFix(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Clear the global replay cache
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Create a test JWT with unique JTI
|
||||
jti := generateRandomString(16)
|
||||
@@ -3064,14 +3075,12 @@ func TestTokenReplayDetectionFalsePositiveFix(t *testing.T) {
|
||||
|
||||
// TestAuthenticationFlowReplayDetection tests the complete authentication flow
|
||||
func TestAuthenticationFlowReplayDetection(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Clear the global replay cache
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Create a test JWT with unique JTI
|
||||
jti := generateRandomString(16)
|
||||
@@ -3139,14 +3148,12 @@ func TestAuthenticationFlowReplayDetection(t *testing.T) {
|
||||
|
||||
// TestActualReplayAttackDetection ensures real replay attacks are still properly detected
|
||||
func TestActualReplayAttackDetection(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Clear the global replay cache
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Create a test JWT with unique JTI
|
||||
jti := generateRandomString(16)
|
||||
@@ -3217,17 +3224,15 @@ func TestActualReplayAttackDetection(t *testing.T) {
|
||||
|
||||
// TestConcurrentTokenValidation tests thread safety of replay detection
|
||||
func TestConcurrentTokenValidation(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Configure rate limiter to allow more requests for concurrent testing
|
||||
ts.tOidc.limiter = rate.NewLimiter(rate.Limit(1000), 1000) // Allow 1000 requests per second with burst of 1000
|
||||
|
||||
// Clear the global replay cache
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Create multiple tokens with unique JTIs
|
||||
var tokens []string
|
||||
@@ -3306,17 +3311,16 @@ func TestConcurrentTokenValidation(t *testing.T) {
|
||||
|
||||
// TestJTIBlacklistBehavior tests the JTI blacklist cache management
|
||||
func TestJTIBlacklistBehavior(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Clear the global replay cache
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
// Properly reinitialize the global replay cache
|
||||
cleanupReplayCache() // Clean up any existing cache and reset sync.Once
|
||||
initReplayCache() // Initialize new cache through proper channel
|
||||
|
||||
// Create a test JWT with unique JTI
|
||||
jti := generateRandomString(16)
|
||||
t.Logf("TestJTIBlacklistBehavior - JTI: %s", jti)
|
||||
now := time.Now()
|
||||
exp := now.Add(1 * time.Hour).Unix()
|
||||
iat := now.Unix()
|
||||
@@ -3355,8 +3359,8 @@ func TestJTIBlacklistBehavior(t *testing.T) {
|
||||
{
|
||||
name: "JTI exists in blacklist after verification",
|
||||
action: func() error {
|
||||
replayCacheMu.Lock()
|
||||
defer replayCacheMu.Unlock()
|
||||
replayCacheMu.RLock()
|
||||
defer replayCacheMu.RUnlock()
|
||||
if _, exists := replayCache.Get(jti); !exists {
|
||||
return fmt.Errorf("JTI not found in blacklist cache")
|
||||
}
|
||||
@@ -3406,14 +3410,12 @@ func TestJTIBlacklistBehavior(t *testing.T) {
|
||||
|
||||
// TestSessionBasedTokenRevalidation tests token revalidation in session-based scenarios
|
||||
func TestSessionBasedTokenRevalidation(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Clear the global replay cache
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
cleanupReplayCache()
|
||||
initReplayCache()
|
||||
|
||||
// Create a test JWT with unique JTI
|
||||
jti := generateRandomString(16)
|
||||
@@ -3480,14 +3482,12 @@ func TestSessionBasedTokenRevalidation(t *testing.T) {
|
||||
|
||||
// TestEdgeCasesWithDifferentTokenTypes tests replay detection with different token types
|
||||
func TestEdgeCasesWithDifferentTokenTypes(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Clear the global replay cache
|
||||
replayCacheMu.Lock()
|
||||
replayCache = NewCache()
|
||||
replayCache.SetMaxSize(10000)
|
||||
replayCacheMu.Unlock()
|
||||
// Properly reinitialize the global replay cache
|
||||
cleanupReplayCache() // Clean up any existing cache and reset sync.Once
|
||||
initReplayCache() // Initialize new cache through proper channel
|
||||
|
||||
now := time.Now()
|
||||
exp := now.Add(1 * time.Hour).Unix()
|
||||
@@ -3969,7 +3969,7 @@ func TestNewWithScopeAppending(t *testing.T) {
|
||||
|
||||
// TestBuildAuthURLWithMergedScopes tests that the auth URL includes the properly merged scopes
|
||||
func TestBuildAuthURLWithMergedScopes(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -4026,7 +4026,7 @@ func TestBuildAuthURLWithMergedScopes(t *testing.T) {
|
||||
// TestBuildAuthURL_OverrideScopes_And_OfflineAccess tests the offline_access logic in buildAuthURL
|
||||
// considering the overrideScopes flag.
|
||||
func TestBuildAuthURL_OverrideScopes_And_OfflineAccess(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup() // Sets up ts.tOidc
|
||||
|
||||
tests := []struct {
|
||||
@@ -4164,7 +4164,7 @@ func TestBuildAuthURL_OverrideScopes_And_OfflineAccess(t *testing.T) {
|
||||
|
||||
// TestBuildAuthURL_SpecificUserCase tests the buildAuthURL function with the specific user-reported scenario.
|
||||
func TestBuildAuthURL_SpecificUserCase(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup() // Basic setup for tOidc
|
||||
|
||||
// Configure the TraefikOidc instance for the specific scenario
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LazyBackgroundTask wraps BackgroundTask to start only when needed
|
||||
type LazyBackgroundTask struct {
|
||||
*BackgroundTask
|
||||
started bool
|
||||
startOnce sync.Once
|
||||
}
|
||||
|
||||
// NewLazyBackgroundTask creates a background task that doesn't start immediately
|
||||
func NewLazyBackgroundTask(name string, interval time.Duration, taskFunc func(), logger *Logger, wg ...*sync.WaitGroup) *LazyBackgroundTask {
|
||||
return &LazyBackgroundTask{
|
||||
BackgroundTask: NewBackgroundTask(name, interval, taskFunc, logger, wg...),
|
||||
started: false,
|
||||
}
|
||||
}
|
||||
|
||||
// StartIfNeeded starts the task only if it hasn't been started yet
|
||||
func (lt *LazyBackgroundTask) StartIfNeeded() {
|
||||
lt.startOnce.Do(func() {
|
||||
if !lt.started {
|
||||
lt.BackgroundTask.Start()
|
||||
lt.started = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Stop stops the task if it was started
|
||||
func (lt *LazyBackgroundTask) Stop() {
|
||||
if lt.started {
|
||||
lt.BackgroundTask.Stop()
|
||||
lt.started = false
|
||||
lt.startOnce = sync.Once{} // Reset for potential restart
|
||||
}
|
||||
}
|
||||
|
||||
// NewLazyCacheWithLogger creates a cache that doesn't start cleanup until first use
|
||||
func NewLazyCacheWithLogger(logger *Logger) *Cache {
|
||||
if logger == nil {
|
||||
logger = newNoOpLogger()
|
||||
}
|
||||
|
||||
c := &Cache{
|
||||
items: make(map[string]CacheItem, DefaultMaxSize),
|
||||
order: list.New(),
|
||||
elems: make(map[string]*list.Element, DefaultMaxSize),
|
||||
maxSize: DefaultMaxSize,
|
||||
autoCleanupInterval: 10 * time.Minute, // Increased from 5 minutes
|
||||
logger: logger,
|
||||
}
|
||||
// Don't start cleanup immediately - it will be started on first use
|
||||
return c
|
||||
}
|
||||
|
||||
// NewLazyCache creates a cache that doesn't start cleanup immediately
|
||||
func NewLazyCache() *Cache {
|
||||
return NewLazyCacheWithLogger(nil)
|
||||
}
|
||||
|
||||
// CleanupIdleConnections periodically closes idle HTTP connections
|
||||
func CleanupIdleConnections(client *http.Client, interval time.Duration, stopChan <-chan struct{}) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
case <-stopChan:
|
||||
// Final cleanup
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OptimizedMiddlewareConfig provides configuration for memory-optimized middleware
|
||||
type OptimizedMiddlewareConfig struct {
|
||||
// DelayBackgroundTasks delays starting background tasks until first request
|
||||
DelayBackgroundTasks bool
|
||||
|
||||
// ReducedCleanupIntervals uses longer intervals for cleanup tasks
|
||||
ReducedCleanupIntervals bool
|
||||
|
||||
// AggressiveConnectionCleanup closes idle connections more aggressively
|
||||
AggressiveConnectionCleanup bool
|
||||
|
||||
// MinimalCacheSize uses smaller default cache sizes
|
||||
MinimalCacheSize bool
|
||||
}
|
||||
|
||||
// DefaultOptimizedConfig returns a configuration optimized for low memory usage
|
||||
func DefaultOptimizedConfig() *OptimizedMiddlewareConfig {
|
||||
return &OptimizedMiddlewareConfig{
|
||||
DelayBackgroundTasks: true,
|
||||
ReducedCleanupIntervals: true,
|
||||
AggressiveConnectionCleanup: true,
|
||||
MinimalCacheSize: true,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -36,7 +36,7 @@ func NewMetadataCacheWithLogger(wg *sync.WaitGroup, logger *Logger) *MetadataCac
|
||||
}
|
||||
|
||||
c := &MetadataCache{
|
||||
autoCleanupInterval: 5 * time.Minute,
|
||||
autoCleanupInterval: 30 * time.Minute, // Increased from 5 minutes since metadata changes rarely
|
||||
logger: logger,
|
||||
wg: wg,
|
||||
stopChan: make(chan struct{}),
|
||||
|
||||
@@ -74,6 +74,9 @@ func TestRefreshGracePeriodConfiguration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTokenRefreshWithinGracePeriod(t *testing.T) {
|
||||
// Reset global state to prevent test interference
|
||||
resetGlobalState()
|
||||
|
||||
refreshCount := int32(0)
|
||||
tokenVersion := int32(1)
|
||||
|
||||
@@ -286,6 +289,9 @@ func TestGracePeriodWithProviderSpecificBehavior(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRefreshGracePeriodConcurrency(t *testing.T) {
|
||||
// Reset global state to prevent test interference
|
||||
resetGlobalState()
|
||||
|
||||
var refreshMutex sync.Mutex
|
||||
refreshCount := 0
|
||||
blockedRequests := int32(0)
|
||||
|
||||
+36
-33
@@ -17,9 +17,12 @@ import (
|
||||
|
||||
// TestConcurrentTokenVerification tests race conditions in token verification
|
||||
func TestConcurrentTokenVerification(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create multiple valid tokens to avoid replay detection
|
||||
tokens := make([]string, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
@@ -40,27 +43,25 @@ func TestConcurrentTokenVerification(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create a fresh instance for this test
|
||||
var goroutineWG sync.WaitGroup
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
tOidc := &TraefikOidc{
|
||||
issuerURL: "https://test-issuer.com",
|
||||
clientID: "test-client-id",
|
||||
jwkCache: ts.mockJWKCache,
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
tokenCache: tokenCache,
|
||||
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high rate limit
|
||||
logger: NewLogger("debug"),
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
httpClient: &http.Client{},
|
||||
extractClaimsFunc: extractClaims,
|
||||
goroutineWG: &goroutineWG,
|
||||
}
|
||||
tOidc.tokenVerifier = tOidc
|
||||
tOidc.jwtVerifier = tOidc
|
||||
|
||||
// Ensure cleanup when test finishes
|
||||
defer func() {
|
||||
if err := tOidc.Close(); err != nil {
|
||||
t.Logf("Error closing TraefikOidc instance: %v", err)
|
||||
}
|
||||
}()
|
||||
tc.addOIDC(tOidc)
|
||||
|
||||
// Test concurrent verification
|
||||
const numGoroutines = 50
|
||||
@@ -130,7 +131,7 @@ func TestConcurrentTokenVerification(t *testing.T) {
|
||||
|
||||
// TestCacheMemoryExhaustion tests cache behavior under memory pressure
|
||||
func TestCacheMemoryExhaustion(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a cache with limited size
|
||||
@@ -372,7 +373,7 @@ func TestProviderFailureRecovery(t *testing.T) {
|
||||
|
||||
// TestOversizedTokenHandling tests boundary value handling
|
||||
func TestOversizedTokenHandling(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create an oversized token with large claims
|
||||
@@ -441,7 +442,7 @@ func TestOversizedTokenHandling(t *testing.T) {
|
||||
|
||||
// TestMaliciousInputValidation tests security input validation
|
||||
func TestMaliciousInputValidation(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
maliciousInputs := []struct {
|
||||
@@ -496,28 +497,29 @@ func TestMaliciousInputValidation(t *testing.T) {
|
||||
|
||||
for _, test := range maliciousInputs {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create cleanup helper for subtest
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create a fresh instance for each test to avoid rate limiting issues
|
||||
var goroutineWG sync.WaitGroup
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
freshOidc := &TraefikOidc{
|
||||
issuerURL: "https://test-issuer.com",
|
||||
clientID: "test-client-id",
|
||||
jwkCache: ts.mockJWKCache,
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
tokenCache: tokenCache,
|
||||
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high rate limit
|
||||
logger: NewLogger("debug"),
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
httpClient: &http.Client{},
|
||||
extractClaimsFunc: extractClaims,
|
||||
goroutineWG: &goroutineWG,
|
||||
}
|
||||
freshOidc.tokenVerifier = freshOidc
|
||||
freshOidc.jwtVerifier = freshOidc
|
||||
|
||||
// Ensure cleanup when test finishes
|
||||
defer func() {
|
||||
if err := freshOidc.Close(); err != nil {
|
||||
t.Logf("Error closing TraefikOidc instance: %v", err)
|
||||
}
|
||||
}()
|
||||
tc.addOIDC(freshOidc)
|
||||
|
||||
// All malicious inputs should be safely rejected
|
||||
err := freshOidc.VerifyToken(test.token)
|
||||
@@ -646,7 +648,7 @@ func TestResourceLimits(t *testing.T) {
|
||||
|
||||
// TestErrorRecoveryPatterns tests various error recovery scenarios
|
||||
func TestErrorRecoveryPatterns(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Test recovery from cache corruption
|
||||
@@ -707,9 +709,12 @@ func TestPerformanceUnderLoad(t *testing.T) {
|
||||
t.Skip("Skipping performance test in short mode")
|
||||
}
|
||||
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create multiple valid tokens
|
||||
const numTokens = 100
|
||||
tokens := make([]string, numTokens)
|
||||
@@ -731,27 +736,25 @@ func TestPerformanceUnderLoad(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create fresh instance with high rate limit
|
||||
var goroutineWG sync.WaitGroup
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
tOidc := &TraefikOidc{
|
||||
issuerURL: "https://test-issuer.com",
|
||||
clientID: "test-client-id",
|
||||
jwkCache: ts.mockJWKCache,
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
tokenCache: tokenCache,
|
||||
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high limit
|
||||
logger: NewLogger("info"), // Reduce logging for performance
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
httpClient: &http.Client{},
|
||||
extractClaimsFunc: extractClaims,
|
||||
goroutineWG: &goroutineWG,
|
||||
}
|
||||
tOidc.tokenVerifier = tOidc
|
||||
tOidc.jwtVerifier = tOidc
|
||||
|
||||
// Ensure cleanup when test finishes
|
||||
defer func() {
|
||||
if err := tOidc.Close(); err != nil {
|
||||
t.Logf("Error closing TraefikOidc instance: %v", err)
|
||||
}
|
||||
}()
|
||||
tc.addOIDC(tOidc)
|
||||
|
||||
// Performance test
|
||||
const iterations = 1000
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@ func TestScopesConfiguration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildAuthURLScopeHandling(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup() // Basic setup for TraefikOidc instance
|
||||
|
||||
// Default scopes expected if not overridden and no user scopes provided
|
||||
|
||||
+51
-25
@@ -21,7 +21,7 @@ import (
|
||||
// TestJWTAlgorithmConfusionAttack tests if the plugin is vulnerable to JWT algorithm confusion attacks
|
||||
// where an attacker might try to switch from an asymmetric algorithm (RS256) to a symmetric one (HS256)
|
||||
func TestJWTAlgorithmConfusionAttack(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a standard JWT with RS256 algorithm
|
||||
@@ -86,7 +86,7 @@ func TestJWTAlgorithmConfusionAttack(t *testing.T) {
|
||||
// TestJWTNoneAlgorithmAttack tests the plugin's resistance to the "none" algorithm attack
|
||||
// where an attacker removes the signature and sets the algorithm to "none"
|
||||
func TestJWTNoneAlgorithmAttack(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a standard JWT
|
||||
@@ -150,7 +150,7 @@ func TestJWTNoneAlgorithmAttack(t *testing.T) {
|
||||
|
||||
// TestJWTTokenTampering tests the plugin's ability to detect modifications to the JWT payload
|
||||
func TestJWTTokenTampering(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a standard JWT
|
||||
@@ -215,7 +215,7 @@ func TestJWTTokenTampering(t *testing.T) {
|
||||
|
||||
// TestJWTExpiredToken tests the plugin's handling of expired tokens
|
||||
func TestJWTExpiredToken(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a JWT that is already expired
|
||||
@@ -248,7 +248,7 @@ func TestJWTExpiredToken(t *testing.T) {
|
||||
|
||||
// TestJWTFutureToken tests the plugin's handling of tokens issued in the future
|
||||
func TestJWTFutureToken(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a JWT with a future issuance time
|
||||
@@ -281,10 +281,13 @@ func TestJWTFutureToken(t *testing.T) {
|
||||
|
||||
// TestJWTReplayAttack tests the plugin's protection against token replay attacks
|
||||
func TestJWTReplayAttack(t *testing.T) {
|
||||
// Create cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create a new instance for this test to avoid interference from global state
|
||||
logger := NewLogger("debug")
|
||||
tokenBlacklist := NewCache()
|
||||
tokenCache := NewTokenCache()
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
|
||||
// Create keys
|
||||
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
@@ -383,7 +386,7 @@ func TestJWTReplayAttack(t *testing.T) {
|
||||
|
||||
// TestMissingClaims tests validation of tokens with missing required claims
|
||||
func TestMissingClaims(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Test cases for missing claims
|
||||
@@ -460,6 +463,9 @@ func TestMissingClaims(t *testing.T) {
|
||||
|
||||
// TestSessionFixationAttack tests the plugin's resistance to session fixation attacks
|
||||
func TestSessionFixationAttack(t *testing.T) {
|
||||
// Create cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
logger := NewLogger("debug")
|
||||
sm, err := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger)
|
||||
if err != nil {
|
||||
@@ -536,6 +542,8 @@ func TestSessionFixationAttack(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create the TraefikOidc middleware
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
tOidc := &TraefikOidc{
|
||||
next: nextHandler,
|
||||
name: "test",
|
||||
@@ -546,8 +554,8 @@ func TestSessionFixationAttack(t *testing.T) {
|
||||
clientSecret: "test-client-secret",
|
||||
jwkCache: mockJWKCache,
|
||||
jwksURL: "https://test-jwks-url.com",
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
tokenCache: tokenCache,
|
||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||
logger: logger,
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
@@ -798,10 +806,13 @@ func TestCSRFProtection(t *testing.T) {
|
||||
|
||||
// TestTokenBlacklisting tests the token blacklisting mechanism
|
||||
func TestTokenBlacklisting(t *testing.T) {
|
||||
// Create cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create a new instance for this test to avoid interference from global state
|
||||
logger := NewLogger("debug")
|
||||
tokenBlacklist := NewCache()
|
||||
tokenCache := NewTokenCache()
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
|
||||
// Create keys
|
||||
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
@@ -886,7 +897,7 @@ func TestTokenBlacklisting(t *testing.T) {
|
||||
|
||||
// TestDifferentSigningAlgorithms tests that the plugin properly handles different signing algorithms
|
||||
func TestDifferentSigningAlgorithms(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Test cases for different algorithms - the implementation actually supports multiple algorithms
|
||||
@@ -1187,7 +1198,7 @@ func createECJWK(privateKey *ecdsa.PrivateKey, alg, kid string) JWK {
|
||||
|
||||
// TestMalformedTokens tests the plugin's handling of malformed tokens
|
||||
func TestMalformedTokens(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
testCases := []struct {
|
||||
@@ -1250,23 +1261,28 @@ func TestMalformedTokens(t *testing.T) {
|
||||
|
||||
// TestRateLimiting tests the rate limiting functionality to prevent brute force attacks
|
||||
func TestRateLimiting(t *testing.T) {
|
||||
// Create cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
// Create a fresh instance for this test to avoid affecting other tests with rate limiting
|
||||
logger := NewLogger("debug")
|
||||
|
||||
// Create a new test suite for this test only
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a separate TraefikOidc instance with a very restrictive rate limiter
|
||||
// This prevents the global instance from being rate-limited
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
tOidc := &TraefikOidc{
|
||||
issuerURL: "https://test-issuer.com",
|
||||
clientID: "test-client-id",
|
||||
clientSecret: "test-client-secret",
|
||||
jwkCache: ts.mockJWKCache,
|
||||
jwksURL: "https://test-jwks-url.com",
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
tokenCache: tokenCache,
|
||||
// Allow only 2 requests per 10 seconds
|
||||
limiter: rate.NewLimiter(rate.Every(10*time.Second), 2),
|
||||
logger: logger,
|
||||
@@ -1347,7 +1363,10 @@ func TestRateLimiting(t *testing.T) {
|
||||
// TestAuthorizationHeaderBypass tests that the plugin correctly handles attempts to bypass
|
||||
// authorization by directly providing an Authorization header
|
||||
func TestAuthorizationHeaderBypass(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
// Create cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a test next handler that would indicate successful authentication
|
||||
@@ -1357,6 +1376,8 @@ func TestAuthorizationHeaderBypass(t *testing.T) {
|
||||
})
|
||||
|
||||
// Create the TraefikOidc instance
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
tOidc := &TraefikOidc{
|
||||
next: nextHandler,
|
||||
name: "test",
|
||||
@@ -1367,8 +1388,8 @@ func TestAuthorizationHeaderBypass(t *testing.T) {
|
||||
clientSecret: "test-client-secret",
|
||||
jwkCache: ts.mockJWKCache,
|
||||
jwksURL: "https://test-jwks-url.com",
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
tokenCache: tokenCache,
|
||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||
logger: NewLogger("debug"),
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
@@ -1419,7 +1440,7 @@ func TestAuthorizationHeaderBypass(t *testing.T) {
|
||||
|
||||
// TestEmptyAudience tests tokens with empty audience claim
|
||||
func TestEmptyAudience(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a JWT with empty audience
|
||||
@@ -1452,7 +1473,7 @@ func TestEmptyAudience(t *testing.T) {
|
||||
|
||||
// TestEmptyIssuer tests tokens with empty issuer claim
|
||||
func TestEmptyIssuer(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a JWT with empty issuer
|
||||
@@ -1485,7 +1506,10 @@ func TestEmptyIssuer(t *testing.T) {
|
||||
|
||||
// TestInvalidRedirectURI tests the plugin's handling of invalid redirect URIs
|
||||
func TestInvalidRedirectURI(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
// Create cleanup helper
|
||||
tc := newTestCleanup(t)
|
||||
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create a test request with an invalid redirect URI
|
||||
@@ -1527,6 +1551,8 @@ func TestInvalidRedirectURI(t *testing.T) {
|
||||
})
|
||||
|
||||
// Create the TraefikOidc instance
|
||||
tokenBlacklist := tc.addCache(NewCache())
|
||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||
tOidc := &TraefikOidc{
|
||||
next: nextHandler,
|
||||
name: "test",
|
||||
@@ -1537,8 +1563,8 @@ func TestInvalidRedirectURI(t *testing.T) {
|
||||
clientSecret: "test-client-secret",
|
||||
jwkCache: ts.mockJWKCache,
|
||||
jwksURL: "https://test-jwks-url.com",
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
tokenCache: tokenCache,
|
||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||
logger: NewLogger("debug"),
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
|
||||
@@ -81,6 +81,7 @@ type SecurityMonitor struct {
|
||||
eventHandlers []SecurityEventHandler
|
||||
config SecurityMonitorConfig
|
||||
ipMutex sync.RWMutex
|
||||
cleanupTask *BackgroundTask // Store cleanup task per instance
|
||||
}
|
||||
|
||||
// IPFailureTracker maintains failure statistics for a specific IP address.
|
||||
@@ -141,9 +142,6 @@ func DefaultSecurityMonitorConfig() SecurityMonitorConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupTask holds the BackgroundTask for security cleanup
|
||||
var cleanupTask *BackgroundTask
|
||||
|
||||
// NewSecurityMonitor creates a new security monitor instance
|
||||
func NewSecurityMonitor(config SecurityMonitorConfig, logger *Logger) *SecurityMonitor {
|
||||
sm := &SecurityMonitor{
|
||||
@@ -489,19 +487,19 @@ func (spd *SuspiciousPatternDetector) DetectSuspiciousPatterns() []string {
|
||||
// startCleanupRoutine starts the background cleanup routine
|
||||
func (sm *SecurityMonitor) startCleanupRoutine() {
|
||||
// Use BackgroundTask abstraction for consistent management
|
||||
cleanupTask = NewBackgroundTask(
|
||||
sm.cleanupTask = NewBackgroundTask(
|
||||
"security-monitor-cleanup",
|
||||
time.Duration(sm.config.CleanupIntervalMinutes)*time.Minute,
|
||||
sm.cleanup,
|
||||
sm.logger)
|
||||
cleanupTask.Start()
|
||||
sm.cleanupTask.Start()
|
||||
}
|
||||
|
||||
// StopCleanupRoutine stops the background cleanup routine
|
||||
func (sm *SecurityMonitor) StopCleanupRoutine() {
|
||||
if cleanupTask != nil {
|
||||
cleanupTask.Stop()
|
||||
cleanupTask = nil
|
||||
if sm.cleanupTask != nil {
|
||||
sm.cleanupTask.Stop()
|
||||
sm.cleanupTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -659,9 +659,11 @@ func (sm *SessionManager) CleanupOldCookies(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
// Mark cleanup as done for this session manager instance
|
||||
if !sm.cleanupDone && len(processedCookies) > 0 {
|
||||
if len(processedCookies) > 0 {
|
||||
sm.cleanupMutex.Lock()
|
||||
sm.cleanupDone = true
|
||||
if !sm.cleanupDone {
|
||||
sm.cleanupDone = true
|
||||
}
|
||||
sm.cleanupMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +252,13 @@ func TestTemplateIntegrationWithPlugin(t *testing.T) {
|
||||
traefikOidc, ok := handler.(*TraefikOidc)
|
||||
require.True(t, ok)
|
||||
|
||||
// Clean up the middleware when test finishes
|
||||
defer func() {
|
||||
if err := traefikOidc.Close(); err != nil {
|
||||
t.Errorf("Failed to close middleware: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create a mock session with claims
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// in the actual middleware flow
|
||||
func TestTemplatedHeadersIntegration(t *testing.T) {
|
||||
// Create a TestSuite to use its helper methods and fields
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
@@ -423,7 +423,7 @@ func TestTemplatedHeadersIntegration(t *testing.T) {
|
||||
// TestEdgeCaseTemplatedHeaders tests edge cases for templated headers
|
||||
func TestEdgeCaseTemplatedHeaders(t *testing.T) {
|
||||
// Create a TestSuite to use its helper methods and fields
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -102,6 +102,13 @@ func TestTraefikConfigurationParsing(t *testing.T) {
|
||||
t.Fatalf("Handler is not of type *TraefikOidc")
|
||||
}
|
||||
|
||||
// Clean up the middleware when test finishes
|
||||
defer func() {
|
||||
if err := middleware.Close(); err != nil {
|
||||
t.Errorf("Failed to close middleware: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Check that templates were parsed correctly
|
||||
if len(tc.config.Headers) > 0 {
|
||||
if len(middleware.headerTemplates) != len(tc.config.Headers) {
|
||||
@@ -232,6 +239,13 @@ func TestIssue55ReproductionAttempt(t *testing.T) {
|
||||
t.Fatalf("Handler is not of type *TraefikOidc")
|
||||
}
|
||||
|
||||
// Clean up the middleware when test finishes
|
||||
defer func() {
|
||||
if err := middleware.Close(); err != nil {
|
||||
t.Errorf("Failed to close middleware: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Check that the header template was parsed
|
||||
if len(middleware.headerTemplates) != 1 {
|
||||
t.Errorf("Expected 1 header template, got %d", len(middleware.headerTemplates))
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +26,125 @@ func (w *testWriter) Write(p []byte) (n int, err error) {
|
||||
|
||||
// Test helper adapters for the new test files
|
||||
|
||||
// resetGlobalState resets all global singletons to prevent test interference
|
||||
func resetGlobalState() {
|
||||
// Reset global cache manager
|
||||
cacheManagerMutex.Lock()
|
||||
if globalCacheManager != nil {
|
||||
globalCacheManager.Close()
|
||||
globalCacheManager = nil
|
||||
}
|
||||
cacheManagerOnce = sync.Once{}
|
||||
cacheManagerMutex.Unlock()
|
||||
|
||||
// Reset and cleanup replay cache
|
||||
cleanupReplayCache()
|
||||
|
||||
// Reset memory pools
|
||||
memoryPoolMutex.Lock()
|
||||
globalMemoryPools = nil
|
||||
memoryPoolOnce = sync.Once{}
|
||||
memoryPoolMutex.Unlock()
|
||||
}
|
||||
|
||||
// testCleanup provides comprehensive cleanup for tests to prevent goroutine leaks
|
||||
type testCleanup struct {
|
||||
t *testing.T
|
||||
caches []*Cache
|
||||
servers []*httptest.Server
|
||||
oidcs []*TraefikOidc
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// newTestCleanup creates a new test cleanup helper that automatically registers cleanup
|
||||
func newTestCleanup(t *testing.T) *testCleanup {
|
||||
tc := &testCleanup{
|
||||
t: t,
|
||||
caches: make([]*Cache, 0),
|
||||
servers: make([]*httptest.Server, 0),
|
||||
oidcs: make([]*TraefikOidc, 0),
|
||||
}
|
||||
|
||||
// Register cleanup to run even if test panics
|
||||
t.Cleanup(func() {
|
||||
tc.cleanupAll()
|
||||
})
|
||||
|
||||
return tc
|
||||
}
|
||||
|
||||
// addCache registers a cache for cleanup
|
||||
func (tc *testCleanup) addCache(c *Cache) *Cache {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
tc.caches = append(tc.caches, c)
|
||||
return c
|
||||
}
|
||||
|
||||
// addTokenCache registers a token cache for cleanup
|
||||
func (tc *testCleanup) addTokenCache(c *TokenCache) *TokenCache {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
if c != nil && c.cache != nil {
|
||||
tc.caches = append(tc.caches, c.cache)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// addServer registers an httptest server for cleanup
|
||||
func (tc *testCleanup) addServer(s *httptest.Server) *httptest.Server {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
tc.servers = append(tc.servers, s)
|
||||
return s
|
||||
}
|
||||
|
||||
// addOIDC registers a TraefikOidc instance for cleanup
|
||||
func (tc *testCleanup) addOIDC(o *TraefikOidc) *TraefikOidc {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
tc.oidcs = append(tc.oidcs, o)
|
||||
return o
|
||||
}
|
||||
|
||||
// cleanupAll cleans up all registered resources
|
||||
func (tc *testCleanup) cleanupAll() {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
|
||||
// Close all caches
|
||||
for _, c := range tc.caches {
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close all servers
|
||||
for _, s := range tc.servers {
|
||||
if s != nil {
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close all OIDC instances
|
||||
for _, o := range tc.oidcs {
|
||||
if o != nil {
|
||||
// Close caches within the OIDC instance
|
||||
if o.tokenCache != nil && o.tokenCache.cache != nil {
|
||||
o.tokenCache.cache.Close()
|
||||
}
|
||||
if o.tokenBlacklist != nil {
|
||||
o.tokenBlacklist.Close()
|
||||
}
|
||||
// Call Close if it exists
|
||||
o.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset global state
|
||||
resetGlobalState()
|
||||
}
|
||||
|
||||
// createTestConfig creates a config with all required fields populated for testing
|
||||
func createTestConfig() *Config {
|
||||
config := CreateConfig()
|
||||
@@ -38,6 +158,9 @@ func createTestConfig() *Config {
|
||||
|
||||
// setupTestOIDCMiddleware creates a test OIDC middleware instance with mock servers
|
||||
func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httptest.Server) {
|
||||
// Reset global state to ensure test isolation
|
||||
resetGlobalState()
|
||||
|
||||
// Create mock OIDC server
|
||||
var serverURL string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -114,6 +237,9 @@ func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httpt
|
||||
testTokenURL := testIssuerURL + "/token"
|
||||
testJWKSURL := testIssuerURL + "/keys"
|
||||
|
||||
// Create WaitGroup for background goroutines
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Create TraefikOidc instance directly
|
||||
oidc := &TraefikOidc{
|
||||
next: nextHandler,
|
||||
@@ -145,6 +271,8 @@ func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httpt
|
||||
jwkCache: &JWKCache{},
|
||||
metadataCache: NewMetadataCache(nil),
|
||||
ctx: context.Background(),
|
||||
goroutineWG: &wg,
|
||||
providerURL: serverURL,
|
||||
}
|
||||
|
||||
// Process excluded URLs
|
||||
@@ -88,7 +88,7 @@ func TestTokenTypeDistinction(t *testing.T) {
|
||||
// TestTokenTypeIntegration tests the integration of ID and access tokens with the middleware
|
||||
func TestTokenTypeIntegration(t *testing.T) {
|
||||
// Create a TestSuite to use its helper methods and fields
|
||||
ts := &TestSuite{t: t}
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Create different tokens for ID and access tokens
|
||||
@@ -157,8 +157,8 @@ func TestTokenTypeIntegration(t *testing.T) {
|
||||
clientSecret: "test-client-secret",
|
||||
jwkCache: ts.mockJWKCache,
|
||||
jwksURL: "https://test-jwks-url.com",
|
||||
tokenBlacklist: NewCache(),
|
||||
tokenCache: NewTokenCache(),
|
||||
tokenBlacklist: ts.tOidc.tokenBlacklist,
|
||||
tokenCache: ts.tOidc.tokenCache,
|
||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||
logger: NewLogger("debug"),
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// UnifiedTestFramework provides comprehensive test management with automatic cleanup
|
||||
type UnifiedTestFramework struct {
|
||||
t *testing.T
|
||||
mu sync.Mutex
|
||||
cleanupFuncs []func()
|
||||
caches []*Cache
|
||||
tokenCaches []*TokenCache
|
||||
servers []*httptest.Server
|
||||
oidcInstances []*TraefikOidc
|
||||
httpClients []*http.Client
|
||||
backgroundTasks []*BackgroundTask
|
||||
goroutineWGs []*sync.WaitGroup
|
||||
contexts []context.Context
|
||||
cancelFuncs []context.CancelFunc
|
||||
}
|
||||
|
||||
// NewUnifiedTestFramework creates a new test framework with automatic cleanup
|
||||
func NewUnifiedTestFramework(t *testing.T) *UnifiedTestFramework {
|
||||
utf := &UnifiedTestFramework{
|
||||
t: t,
|
||||
cleanupFuncs: make([]func(), 0),
|
||||
caches: make([]*Cache, 0),
|
||||
tokenCaches: make([]*TokenCache, 0),
|
||||
servers: make([]*httptest.Server, 0),
|
||||
oidcInstances: make([]*TraefikOidc, 0),
|
||||
httpClients: make([]*http.Client, 0),
|
||||
backgroundTasks: make([]*BackgroundTask, 0),
|
||||
goroutineWGs: make([]*sync.WaitGroup, 0),
|
||||
contexts: make([]context.Context, 0),
|
||||
cancelFuncs: make([]context.CancelFunc, 0),
|
||||
}
|
||||
|
||||
// Register cleanup to run even if test panics
|
||||
t.Cleanup(func() {
|
||||
utf.CleanupAll()
|
||||
})
|
||||
|
||||
// Reset global state at start
|
||||
resetGlobalState()
|
||||
|
||||
return utf
|
||||
}
|
||||
|
||||
// AddCleanupFunc adds a custom cleanup function
|
||||
func (utf *UnifiedTestFramework) AddCleanupFunc(fn func()) {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
utf.cleanupFuncs = append(utf.cleanupFuncs, fn)
|
||||
}
|
||||
|
||||
// NewCache creates and tracks a cache for cleanup
|
||||
func (utf *UnifiedTestFramework) NewCache() *Cache {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
// Use NewLazyCache to avoid immediate background task start
|
||||
cache := NewLazyCache()
|
||||
utf.caches = append(utf.caches, cache)
|
||||
return cache
|
||||
}
|
||||
|
||||
// NewCacheWithLogger creates and tracks a cache with logger
|
||||
func (utf *UnifiedTestFramework) NewCacheWithLogger(logger *Logger) *Cache {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
// Use lazy cache to avoid immediate background task
|
||||
cache := NewLazyCacheWithLogger(logger)
|
||||
utf.caches = append(utf.caches, cache)
|
||||
return cache
|
||||
}
|
||||
|
||||
// NewTokenCache creates and tracks a token cache for cleanup
|
||||
func (utf *UnifiedTestFramework) NewTokenCache() *TokenCache {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
// Create token cache with lazy initialization
|
||||
tokenCache := &TokenCache{
|
||||
cache: NewLazyCache(),
|
||||
}
|
||||
utf.tokenCaches = append(utf.tokenCaches, tokenCache)
|
||||
return tokenCache
|
||||
}
|
||||
|
||||
// NewHTTPTestServer creates and tracks an httptest server
|
||||
func (utf *UnifiedTestFramework) NewHTTPTestServer(handler http.Handler) *httptest.Server {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
server := httptest.NewServer(handler)
|
||||
utf.servers = append(utf.servers, server)
|
||||
return server
|
||||
}
|
||||
|
||||
// NewHTTPClient creates and tracks an HTTP client
|
||||
func (utf *UnifiedTestFramework) NewHTTPClient() *http.Client {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 2,
|
||||
IdleConnTimeout: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
utf.httpClients = append(utf.httpClients, client)
|
||||
return client
|
||||
}
|
||||
|
||||
// NewContext creates and tracks a context with cancel
|
||||
func (utf *UnifiedTestFramework) NewContext() context.Context {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
utf.contexts = append(utf.contexts, ctx)
|
||||
utf.cancelFuncs = append(utf.cancelFuncs, cancel)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// NewWaitGroup creates and tracks a wait group
|
||||
func (utf *UnifiedTestFramework) NewWaitGroup() *sync.WaitGroup {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
utf.goroutineWGs = append(utf.goroutineWGs, wg)
|
||||
return wg
|
||||
}
|
||||
|
||||
// NewBackgroundTask creates and tracks a background task
|
||||
func (utf *UnifiedTestFramework) NewBackgroundTask(name string, interval time.Duration, taskFunc func(), logger *Logger) *BackgroundTask {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
task := NewBackgroundTask(name, interval, taskFunc, logger)
|
||||
utf.backgroundTasks = append(utf.backgroundTasks, task)
|
||||
return task
|
||||
}
|
||||
|
||||
// AddOIDCInstance tracks a TraefikOidc instance for cleanup
|
||||
func (utf *UnifiedTestFramework) AddOIDCInstance(oidc *TraefikOidc) *TraefikOidc {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
utf.oidcInstances = append(utf.oidcInstances, oidc)
|
||||
return oidc
|
||||
}
|
||||
|
||||
// CleanupAll performs comprehensive cleanup of all tracked resources
|
||||
func (utf *UnifiedTestFramework) CleanupAll() {
|
||||
utf.mu.Lock()
|
||||
defer utf.mu.Unlock()
|
||||
|
||||
utf.t.Logf("Starting unified test framework cleanup...")
|
||||
|
||||
// Cancel all contexts first to signal shutdown
|
||||
for _, cancel := range utf.cancelFuncs {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop all background tasks
|
||||
for _, task := range utf.backgroundTasks {
|
||||
if task != nil {
|
||||
task.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Close all OIDC instances
|
||||
for _, oidc := range utf.oidcInstances {
|
||||
if oidc != nil {
|
||||
if err := oidc.Close(); err != nil {
|
||||
utf.t.Logf("Error closing OIDC instance: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close all token caches
|
||||
for _, tc := range utf.tokenCaches {
|
||||
if tc != nil && tc.cache != nil {
|
||||
tc.cache.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close all caches
|
||||
for _, c := range utf.caches {
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close all HTTP servers
|
||||
for _, s := range utf.servers {
|
||||
if s != nil {
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close idle connections in HTTP clients
|
||||
for _, client := range utf.httpClients {
|
||||
if client != nil && client.Transport != nil {
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all goroutines with timeout
|
||||
for _, wg := range utf.goroutineWGs {
|
||||
if wg != nil {
|
||||
done := make(chan struct{})
|
||||
go func(wg *sync.WaitGroup) {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}(wg)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Goroutines finished
|
||||
case <-time.After(5 * time.Second):
|
||||
utf.t.Logf("Timeout waiting for goroutines to finish")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run custom cleanup functions
|
||||
for _, fn := range utf.cleanupFuncs {
|
||||
if fn != nil {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset global state
|
||||
resetGlobalState()
|
||||
|
||||
utf.t.Logf("Unified test framework cleanup completed")
|
||||
}
|
||||
|
||||
// EnhancedTestSuite extends TestSuite with UnifiedTestFramework
|
||||
type EnhancedTestSuite struct {
|
||||
*TestSuite
|
||||
*UnifiedTestFramework
|
||||
}
|
||||
|
||||
// NewEnhancedTestSuite creates a test suite with automatic resource management
|
||||
func NewEnhancedTestSuite(t *testing.T) *EnhancedTestSuite {
|
||||
utf := NewUnifiedTestFramework(t)
|
||||
|
||||
ts := &TestSuite{
|
||||
t: t,
|
||||
utf: utf,
|
||||
}
|
||||
|
||||
ets := &EnhancedTestSuite{
|
||||
TestSuite: ts,
|
||||
UnifiedTestFramework: utf,
|
||||
}
|
||||
|
||||
return ets
|
||||
}
|
||||
|
||||
// Setup initializes the enhanced test suite with automatic cleanup
|
||||
func (ets *EnhancedTestSuite) Setup() {
|
||||
var err error
|
||||
|
||||
// Generate RSA key
|
||||
ets.rsaPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
ets.TestSuite.t.Fatalf("Failed to generate RSA key: %v", err)
|
||||
}
|
||||
ets.rsaPublicKey = &ets.rsaPrivateKey.PublicKey
|
||||
|
||||
// Generate EC key
|
||||
ets.ecPrivateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
ets.TestSuite.t.Fatalf("Failed to generate EC key: %v", err)
|
||||
}
|
||||
|
||||
// Create JWK for RSA public key
|
||||
jwk := JWK{
|
||||
Kty: "RSA",
|
||||
Kid: "test-key-id",
|
||||
Alg: "RS256",
|
||||
N: base64.RawURLEncoding.EncodeToString(ets.rsaPublicKey.N.Bytes()),
|
||||
E: base64.RawURLEncoding.EncodeToString(bigIntToBytes(big.NewInt(int64(ets.rsaPublicKey.E)))),
|
||||
}
|
||||
jwks := &JWKSet{
|
||||
Keys: []JWK{jwk},
|
||||
}
|
||||
|
||||
// Create mock JWK cache
|
||||
ets.mockJWKCache = &MockJWKCache{
|
||||
JWKS: jwks,
|
||||
Err: nil,
|
||||
}
|
||||
|
||||
// Create test JWT token
|
||||
now := time.Now()
|
||||
ets.token, err = createTestJWT(ets.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||
"iss": "https://test-issuer.com",
|
||||
"aud": "test-client-id",
|
||||
"exp": now.Add(1 * time.Hour).Unix(),
|
||||
"iat": now.Add(-2 * time.Minute).Unix(),
|
||||
"nbf": now.Add(-2 * time.Minute).Unix(),
|
||||
"sub": "test-subject",
|
||||
"email": "[email protected]",
|
||||
"nonce": "test-nonce",
|
||||
"jti": generateRandomString(16),
|
||||
})
|
||||
if err != nil {
|
||||
ets.TestSuite.t.Fatalf("Failed to create test JWT: %v", err)
|
||||
}
|
||||
|
||||
// Create session manager
|
||||
logger := NewLogger("info")
|
||||
ets.sessionManager, _ = NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger)
|
||||
|
||||
// Create WaitGroup for the OIDC instance
|
||||
goroutineWG := ets.NewWaitGroup()
|
||||
|
||||
// Create TraefikOidc instance with tracked resources
|
||||
ets.tOidc = &TraefikOidc{
|
||||
issuerURL: "https://test-issuer.com",
|
||||
clientID: "test-client-id",
|
||||
clientSecret: "test-client-secret",
|
||||
jwkCache: ets.mockJWKCache,
|
||||
jwksURL: "https://test-jwks-url.com",
|
||||
revocationURL: "https://revocation-endpoint.com",
|
||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||
tokenBlacklist: ets.NewCache(), // Use framework's tracked cache
|
||||
tokenCache: ets.NewTokenCache(), // Use framework's tracked token cache
|
||||
logger: logger,
|
||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||
excludedURLs: map[string]struct{}{"/favicon": {}},
|
||||
httpClient: ets.NewHTTPClient(), // Use framework's tracked HTTP client
|
||||
redirURLPath: "/callback",
|
||||
logoutURLPath: "/callback/logout",
|
||||
tokenURL: "https://test-issuer.com/token",
|
||||
extractClaimsFunc: extractClaims,
|
||||
initComplete: make(chan struct{}),
|
||||
sessionManager: ets.sessionManager,
|
||||
goroutineWG: goroutineWG,
|
||||
ctx: ets.NewContext(), // Use framework's tracked context
|
||||
tokenCleanupStopChan: make(chan struct{}),
|
||||
metadataRefreshStopChan: make(chan struct{}),
|
||||
}
|
||||
close(ets.tOidc.initComplete)
|
||||
|
||||
ets.tOidc.tokenVerifier = ets.tOidc
|
||||
ets.tOidc.jwtVerifier = ets.tOidc
|
||||
|
||||
// Set default mock exchanger
|
||||
ets.tOidc.tokenExchanger = &MockTokenExchanger{
|
||||
ExchangeCodeFunc: func(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) {
|
||||
return &TokenResponse{
|
||||
IDToken: ets.token,
|
||||
AccessToken: ets.token,
|
||||
RefreshToken: "default-refresh-token",
|
||||
ExpiresIn: 3600,
|
||||
}, nil
|
||||
},
|
||||
RefreshTokenFunc: func(refreshToken string) (*TokenResponse, error) {
|
||||
return nil, fmt.Errorf("default mock: refresh not expected")
|
||||
},
|
||||
RevokeTokenFunc: func(token, tokenType string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Track the OIDC instance for cleanup
|
||||
ets.AddOIDCInstance(ets.tOidc)
|
||||
}
|
||||
|
||||
// CreateMockOIDCServer creates a mock OIDC server with automatic cleanup
|
||||
func (utf *UnifiedTestFramework) CreateMockOIDCServer(issuerURL string) *httptest.Server {
|
||||
server := utf.NewHTTPTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/.well-known/openid-configuration":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{
|
||||
"issuer": "%s",
|
||||
"authorization_endpoint": "%s/auth",
|
||||
"token_endpoint": "%s/token",
|
||||
"userinfo_endpoint": "%s/userinfo",
|
||||
"jwks_uri": "%s/keys",
|
||||
"revocation_endpoint": "%s/revoke"
|
||||
}`, issuerURL, issuerURL, issuerURL, issuerURL, issuerURL, issuerURL)
|
||||
case "/keys":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{
|
||||
"keys": [{
|
||||
"kty": "RSA",
|
||||
"kid": "test-key-id",
|
||||
"use": "sig",
|
||||
"n": "test-n-value",
|
||||
"e": "AQAB"
|
||||
}]
|
||||
}`))
|
||||
case "/token":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{
|
||||
"access_token": "test-access-token",
|
||||
"id_token": "` + ValidIDToken + `",
|
||||
"refresh_token": "test-refresh-token",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600
|
||||
}`))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
return server
|
||||
}
|
||||
Reference in New Issue
Block a user