mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
Further pursue of perfection.
This commit is contained in:
+47
-23
@@ -9,12 +9,16 @@ import (
|
|||||||
// It provides a clean interface for starting and stopping periodic operations
|
// It provides a clean interface for starting and stopping periodic operations
|
||||||
// with proper lifecycle management and logging.
|
// with proper lifecycle management and logging.
|
||||||
type BackgroundTask struct {
|
type BackgroundTask struct {
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
taskFunc func()
|
taskFunc func()
|
||||||
logger *Logger
|
logger *Logger
|
||||||
name string
|
name string
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
wg *sync.WaitGroup
|
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.
|
// NewBackgroundTask creates a new background task with the specified parameters.
|
||||||
@@ -29,37 +33,54 @@ type BackgroundTask struct {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - A configured BackgroundTask ready to be started.
|
// - A configured BackgroundTask ready to be started.
|
||||||
func NewBackgroundTask(name string, interval time.Duration, taskFunc func(), logger *Logger, wg ...*sync.WaitGroup) *BackgroundTask {
|
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 {
|
if len(wg) > 0 {
|
||||||
waitGroup = wg[0]
|
externalWG = wg[0]
|
||||||
}
|
}
|
||||||
return &BackgroundTask{
|
return &BackgroundTask{
|
||||||
name: name,
|
name: name,
|
||||||
interval: interval,
|
interval: interval,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
taskFunc: taskFunc,
|
taskFunc: taskFunc,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
wg: waitGroup,
|
externalWG: externalWG,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start begins the background task execution in a separate goroutine.
|
// Start begins the background task execution in a separate goroutine.
|
||||||
// The task runs immediately upon start and then at the specified interval.
|
// The task runs immediately upon start and then at the specified interval.
|
||||||
func (bt *BackgroundTask) Start() {
|
func (bt *BackgroundTask) Start() {
|
||||||
if bt.wg != nil {
|
bt.mu.Lock()
|
||||||
bt.wg.Add(1)
|
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()
|
go bt.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop gracefully terminates the background task by closing the stop channel.
|
// 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.
|
// This method is safe to call multiple times.
|
||||||
func (bt *BackgroundTask) Stop() {
|
func (bt *BackgroundTask) Stop() {
|
||||||
close(bt.stopChan)
|
bt.stopOnce.Do(func() {
|
||||||
if bt.wg != nil {
|
bt.mu.Lock()
|
||||||
bt.wg.Wait()
|
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.
|
// run is the main execution loop for the background task.
|
||||||
@@ -67,8 +88,11 @@ func (bt *BackgroundTask) Stop() {
|
|||||||
// until the stop signal is received.
|
// until the stop signal is received.
|
||||||
func (bt *BackgroundTask) run() {
|
func (bt *BackgroundTask) run() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if bt.wg != nil {
|
// Always decrement internal WaitGroup
|
||||||
bt.wg.Done()
|
bt.internalWG.Done()
|
||||||
|
// Decrement external WaitGroup if provided
|
||||||
|
if bt.externalWG != nil {
|
||||||
|
bt.externalWG.Done()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
ticker := time.NewTicker(bt.interval)
|
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) {
|
func TestAzureOIDCRegression(t *testing.T) {
|
||||||
|
// Create test cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
// Create a mocked TraefikOidc instance configured for Azure AD
|
// Create a mocked TraefikOidc instance configured for Azure AD
|
||||||
mockLogger := NewLogger("debug")
|
mockLogger := NewLogger("debug")
|
||||||
|
|
||||||
|
// Create caches with cleanup tracking
|
||||||
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
|
|
||||||
// Configure for Azure AD provider
|
// Configure for Azure AD provider
|
||||||
baseOidc := &TraefikOidc{
|
baseOidc := &TraefikOidc{
|
||||||
issuerURL: "https://login.microsoftonline.com/tenant-id/v2.0",
|
issuerURL: "https://login.microsoftonline.com/tenant-id/v2.0",
|
||||||
@@ -58,8 +65,8 @@ func TestAzureOIDCRegression(t *testing.T) {
|
|||||||
logger: mockLogger,
|
logger: mockLogger,
|
||||||
httpClient: createDefaultHTTPClient(), // Add HTTP client
|
httpClient: createDefaultHTTPClient(), // Add HTTP client
|
||||||
jwkCache: &JWKCache{}, // Add JWK cache
|
jwkCache: &JWKCache{}, // Add JWK cache
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: tokenCache,
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: tokenBlacklist,
|
||||||
allowedUserDomains: make(map[string]struct{}),
|
allowedUserDomains: make(map[string]struct{}),
|
||||||
allowedUsers: make(map[string]struct{}),
|
allowedUsers: make(map[string]struct{}),
|
||||||
allowedRolesAndGroups: make(map[string]struct{}),
|
allowedRolesAndGroups: make(map[string]struct{}),
|
||||||
|
|||||||
+27
-28
@@ -7,45 +7,36 @@ import (
|
|||||||
|
|
||||||
func TestCache_Cleanup(t *testing.T) {
|
func TestCache_Cleanup(t *testing.T) {
|
||||||
c := NewCache()
|
c := NewCache()
|
||||||
|
defer c.Close() // Stop the auto-cleanup goroutine
|
||||||
|
|
||||||
// Add some items with different expiration times
|
// Add some items with different expiration times
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
pastTime := now.Add(-1 * time.Hour) // Already expired
|
pastTime := now.Add(-1 * time.Hour) // Already expired
|
||||||
futureTime := now.Add(1 * time.Hour) // Not expired
|
futureTime := now.Add(1 * time.Hour) // Not expired
|
||||||
|
|
||||||
// Create test items
|
// Use the Set method to properly add items with synchronization
|
||||||
c.items["expired"] = CacheItem{
|
c.Set("expired", "expired-value", pastTime.Sub(now))
|
||||||
Value: "expired-value",
|
c.Set("valid", "valid-value", futureTime.Sub(now))
|
||||||
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"})
|
|
||||||
|
|
||||||
// Call cleanup, which should only remove expired items
|
// Call cleanup, which should only remove expired items
|
||||||
c.Cleanup()
|
c.Cleanup()
|
||||||
|
|
||||||
// Check that only the expired item was removed
|
// 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()")
|
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()")
|
t.Error("Valid item was incorrectly removed by Cleanup()")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_SetMaxSize(t *testing.T) {
|
func TestCache_SetMaxSize(t *testing.T) {
|
||||||
c := NewCache()
|
c := NewCache()
|
||||||
|
defer c.Close() // Stop the auto-cleanup goroutine
|
||||||
|
|
||||||
// Set a lower max size
|
// Set a lower max size
|
||||||
originalMaxSize := c.maxSize
|
originalMaxSize := 10
|
||||||
newMaxSize := 3
|
newMaxSize := 3
|
||||||
|
|
||||||
// Add more items than the new max size
|
// 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)
|
c.Set(key, i, 1*time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify items were added
|
// Verify items were added by checking a few keys
|
||||||
if len(c.items) != originalMaxSize {
|
for i := 0; i < originalMaxSize; i++ {
|
||||||
t.Errorf("Expected %d items before SetMaxSize, got %d", originalMaxSize, len(c.items))
|
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
|
// Change the max size to a smaller value
|
||||||
c.SetMaxSize(newMaxSize)
|
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
|
// Check that the cache was reduced to the new max size
|
||||||
if len(c.items) > newMaxSize {
|
if count > newMaxSize {
|
||||||
t.Errorf("Cache size %d exceeds new max size %d after SetMaxSize", len(c.items), newMaxSize)
|
t.Errorf("Cache size %d exceeds new max size %d after SetMaxSize", count, newMaxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.maxSize != newMaxSize {
|
// Check that the oldest items were evicted (keyA should be evicted)
|
||||||
t.Errorf("Cache maxSize not updated, expected %d, got %d", newMaxSize, c.maxSize)
|
if _, exists := c.Get("keyA"); exists {
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the oldest items were evicted (should keep "keyC", "keyD", "keyE", etc.)
|
|
||||||
if _, exists := c.items["keyA"]; exists {
|
|
||||||
t.Error("Expected oldest item 'keyA' to be evicted, but it still exists")
|
t.Error("Expected oldest item 'keyA' to be evicted, but it still exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package traefikoidc
|
package traefikoidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -32,39 +31,24 @@ func TestBackgroundGoroutineLeaks(t *testing.T) {
|
|||||||
|
|
||||||
t.Logf("Baseline: Memory=%d KB, Goroutines=%d", baselineAlloc/1024, baselineGoroutines)
|
t.Logf("Baseline: Memory=%d KB, Goroutines=%d", baselineAlloc/1024, baselineGoroutines)
|
||||||
|
|
||||||
// Create middleware instance
|
// Create test cleanup
|
||||||
config := CreateConfig()
|
tc := newTestCleanup(t)
|
||||||
config.ProviderURL = "https://example.com"
|
|
||||||
config.ClientID = "test-client"
|
// Create middleware instance with mock setup
|
||||||
config.ClientSecret = "test-secret"
|
config := createTestConfig()
|
||||||
config.SessionEncryptionKey = "test-encryption-key-that-is-long-enough-32bytes"
|
|
||||||
config.LogLevel = "error" // Reduce noise
|
config.LogLevel = "error" // Reduce noise
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
middleware, server := setupTestOIDCMiddleware(t, config)
|
||||||
defer cancel()
|
tc.addServer(server)
|
||||||
|
tc.addOIDC(middleware)
|
||||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
handler, err := New(ctx, next, config, "test-middleware")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cast to TraefikOidc to access internals
|
|
||||||
middleware, ok := handler.(*TraefikOidc)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Failed to cast to TraefikOidc")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let it sit idle for a while - simulating no requests
|
// Let it sit idle for a while - simulating no requests
|
||||||
// During this time, background goroutines are running
|
// During this time, background goroutines are running
|
||||||
t.Log("Letting middleware sit idle for 30 seconds...")
|
t.Log("Letting middleware sit idle for 3 seconds...")
|
||||||
|
|
||||||
// Take measurements every 5 seconds
|
// Take measurements every 1 second
|
||||||
for i := 0; i < 6; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
@@ -75,12 +59,12 @@ func TestBackgroundGoroutineLeaks(t *testing.T) {
|
|||||||
goroutineIncrease := currentGoroutines - baselineGoroutines
|
goroutineIncrease := currentGoroutines - baselineGoroutines
|
||||||
|
|
||||||
t.Logf("After %d seconds: Memory increase=%.2f MB, Goroutine increase=%d",
|
t.Logf("After %d seconds: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||||
(i+1)*5, allocIncrease, goroutineIncrease)
|
(i + 1), allocIncrease, goroutineIncrease)
|
||||||
|
|
||||||
// Check for significant memory growth (more than 5MB)
|
// Check for significant memory growth (more than 5MB)
|
||||||
if allocIncrease > 5.0 {
|
if allocIncrease > 5.0 {
|
||||||
t.Errorf("Significant memory increase detected: %.2f MB after %d seconds of idle",
|
t.Errorf("Significant memory increase detected: %.2f MB after %d seconds of idle",
|
||||||
allocIncrease, (i+1)*5)
|
allocIncrease, (i + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for goroutine leaks (more than 10 extra goroutines)
|
// Check for goroutine leaks (more than 10 extra goroutines)
|
||||||
@@ -91,8 +75,7 @@ func TestBackgroundGoroutineLeaks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
err = middleware.Close()
|
if err := middleware.Close(); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to close middleware: %v", err)
|
t.Errorf("Failed to close middleware: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +154,8 @@ func TestHTTPClientConnectionLeaks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let connections sit idle (now only 5s with our optimizations)
|
// Let connections sit idle briefly to test cleanup
|
||||||
time.Sleep(6 * time.Second) // Slightly longer than new IdleConnTimeout (5s)
|
time.Sleep(1 * time.Second) // Reduced for faster tests
|
||||||
|
|
||||||
// Force cleanup
|
// Force cleanup
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
@@ -188,10 +171,25 @@ func TestHTTPClientConnectionLeaks(t *testing.T) {
|
|||||||
// TestCacheBackgroundTaskLeaks tests that cache background tasks don't leak
|
// TestCacheBackgroundTaskLeaks tests that cache background tasks don't leak
|
||||||
func TestCacheBackgroundTaskLeaks(t *testing.T) {
|
func TestCacheBackgroundTaskLeaks(t *testing.T) {
|
||||||
t.Run("Multiple cache instances with cleanup tasks", func(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()
|
initialGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
// Create many cache instances
|
// Create many cache instances
|
||||||
caches := make([]*Cache, 50)
|
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++ {
|
for i := 0; i < 50; i++ {
|
||||||
caches[i] = NewCache()
|
caches[i] = NewCache()
|
||||||
// Add some data
|
// Add some data
|
||||||
@@ -220,13 +218,25 @@ func TestCacheBackgroundTaskLeaks(t *testing.T) {
|
|||||||
cache.Close()
|
cache.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for goroutines to stop
|
// Wait for goroutines to stop with timeout
|
||||||
time.Sleep(500 * time.Millisecond)
|
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()
|
finalGoroutines := runtime.NumGoroutine()
|
||||||
remainingGoroutines := finalGoroutines - initialGoroutines
|
remainingGoroutines := finalGoroutines - initialGoroutines
|
||||||
|
|
||||||
if remainingGoroutines > 5 { // Allow small tolerance
|
if !success && remainingGoroutines > 5 { // Allow small tolerance
|
||||||
t.Errorf("Cache cleanup goroutines not stopped properly: %d extra goroutines remain",
|
t.Errorf("Cache cleanup goroutines not stopped properly: %d extra goroutines remain",
|
||||||
remainingGoroutines)
|
remainingGoroutines)
|
||||||
}
|
}
|
||||||
@@ -295,7 +305,15 @@ func TestGlobalSingletonMemoryGrowth(t *testing.T) {
|
|||||||
runtime.GC()
|
runtime.GC()
|
||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
finalAlloc := m.Alloc
|
finalAlloc := m.Alloc
|
||||||
finalAllocIncrease := float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
|
||||||
|
// 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)
|
t.Logf("Final memory increase after cleanup: %.2f MB", finalAllocIncrease)
|
||||||
|
|
||||||
@@ -592,8 +610,8 @@ func TestConcurrentMemoryLeaks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Let it run and measure periodically
|
// Let it run and measure periodically
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
@@ -604,12 +622,12 @@ func TestConcurrentMemoryLeaks(t *testing.T) {
|
|||||||
goroutineIncrease := currentGoroutines - baselineGoroutines
|
goroutineIncrease := currentGoroutines - baselineGoroutines
|
||||||
|
|
||||||
t.Logf("After %d seconds of concurrent load: Memory increase=%.2f MB, Goroutine increase=%d",
|
t.Logf("After %d seconds of concurrent load: Memory increase=%.2f MB, Goroutine increase=%d",
|
||||||
(i+1)*5, allocIncrease, goroutineIncrease)
|
(i + 1), allocIncrease, goroutineIncrease)
|
||||||
|
|
||||||
// Under sustained concurrent load, memory should stabilize
|
// Under sustained concurrent load, memory should stabilize
|
||||||
if i > 2 && allocIncrease > 20.0 {
|
if i > 1 && allocIncrease > 20.0 {
|
||||||
t.Errorf("Memory leak under concurrent load: %.2f MB after %d seconds",
|
t.Errorf("Memory leak under concurrent load: %.2f MB after %d seconds",
|
||||||
allocIncrease, (i+1)*5)
|
allocIncrease, (i + 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -349,6 +349,9 @@ func TestGoogleOIDCRefreshTokenHandling(t *testing.T) {
|
|||||||
Err: nil,
|
Err: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create test cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
// Create a complete test instance with all required fields
|
// Create a complete test instance with all required fields
|
||||||
mockLogger := NewLogger("debug")
|
mockLogger := NewLogger("debug")
|
||||||
googleTOidc := &TraefikOidc{
|
googleTOidc := &TraefikOidc{
|
||||||
@@ -358,8 +361,8 @@ func TestGoogleOIDCRefreshTokenHandling(t *testing.T) {
|
|||||||
logger: mockLogger,
|
logger: mockLogger,
|
||||||
scopes: []string{"openid", "profile", "email"},
|
scopes: []string{"openid", "profile", "email"},
|
||||||
refreshGracePeriod: 60,
|
refreshGracePeriod: 60,
|
||||||
tokenCache: NewTokenCache(), // Initialize tokenCache
|
tokenCache: tc.addTokenCache(NewTokenCache()), // Initialize tokenCache with cleanup
|
||||||
tokenBlacklist: NewCache(), // Initialize tokenBlacklist
|
tokenBlacklist: tc.addCache(NewCache()), // Initialize tokenBlacklist with cleanup
|
||||||
enablePKCE: false,
|
enablePKCE: false,
|
||||||
limiter: rate.NewLimiter(rate.Inf, 0), // No rate limiting for tests
|
limiter: rate.NewLimiter(rate.Inf, 0), // No rate limiting for tests
|
||||||
jwkCache: mockJWKCache,
|
jwkCache: mockJWKCache,
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
replayCacheMu sync.RWMutex // Use RWMutex for better read performance
|
replayCacheMu sync.RWMutex // Use RWMutex for better read performance
|
||||||
replayCache *Cache // Replace unbounded map with bounded Cache
|
replayCache *Cache // Replace unbounded map with bounded Cache
|
||||||
replayCacheOnce sync.Once
|
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.
|
// 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
|
// It acquires a write lock, closes the cache, and sets it to nil
|
||||||
// to ensure proper cleanup during shutdown.
|
// to ensure proper cleanup during shutdown.
|
||||||
func cleanupReplayCache() {
|
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()
|
replayCacheMu.Lock()
|
||||||
defer replayCacheMu.Unlock()
|
defer replayCacheMu.Unlock()
|
||||||
|
|
||||||
@@ -73,7 +89,21 @@ func getReplayCacheStats() (size int, maxSize int) {
|
|||||||
// - ctx: Context for cancellation.
|
// - ctx: Context for cancellation.
|
||||||
// - logger: Logger for debug output (can be nil).
|
// - logger: Logger for debug output (can be nil).
|
||||||
func startReplayCacheCleanup(ctx context.Context, logger *Logger) {
|
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() {
|
go func() {
|
||||||
|
defer replayCacheCleanupWG.Done()
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -91,8 +121,7 @@ func startReplayCacheCleanup(ctx context.Context, logger *Logger) {
|
|||||||
}
|
}
|
||||||
replayCacheMu.RUnlock()
|
replayCacheMu.RUnlock()
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-replayCacheCtx.Done():
|
||||||
cleanupReplayCache()
|
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
logger.Debug("Replay cache cleanup goroutine stopped due to context cancellation")
|
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]
|
shouldSkipReplay := len(skipReplayCheck) > 0 && skipReplayCheck[0]
|
||||||
|
|
||||||
if jti, ok := claims["jti"].(string); ok && !shouldSkipReplay {
|
jtiValue, jtiOk := claims["jti"].(string)
|
||||||
if j.Token == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if jtiOk && !shouldSkipReplay && jtiValue != "" {
|
||||||
initReplayCache()
|
initReplayCache()
|
||||||
|
|
||||||
replayCacheMu.RLock()
|
replayCacheMu.RLock()
|
||||||
_, exists := replayCache.Get(jti)
|
_, exists := replayCache.Get(jtiValue)
|
||||||
replayCacheMu.RUnlock()
|
replayCacheMu.RUnlock()
|
||||||
|
|
||||||
if exists {
|
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)
|
expFloat, ok := claims["exp"].(float64)
|
||||||
@@ -313,7 +340,7 @@ func (j *JWT) Verify(issuerURL, clientID string, skipReplayCheck ...bool) error
|
|||||||
if duration > 0 {
|
if duration > 0 {
|
||||||
replayCacheMu.Lock()
|
replayCacheMu.Lock()
|
||||||
if replayCache != nil {
|
if replayCache != nil {
|
||||||
replayCache.Set(jti, true, duration)
|
replayCache.Set(jtiValue, true, duration)
|
||||||
}
|
}
|
||||||
replayCacheMu.Unlock()
|
replayCacheMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1074,7 +1074,7 @@ func fetchMetadata(wellKnownURL string, httpClient *http.Client) (*ProviderMetad
|
|||||||
// - Header injection for authenticated requests
|
// - Header injection for authenticated requests
|
||||||
func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
// Start background tasks on first request (except for health checks)
|
// Start background tasks on first request (except for health checks)
|
||||||
if !t.firstRequestReceived && !strings.HasPrefix(req.URL.Path, "/health") {
|
if !strings.HasPrefix(req.URL.Path, "/health") {
|
||||||
t.firstRequestMutex.Lock()
|
t.firstRequestMutex.Lock()
|
||||||
if !t.firstRequestReceived {
|
if !t.firstRequestReceived {
|
||||||
t.firstRequestReceived = true
|
t.firstRequestReceived = true
|
||||||
|
|||||||
+3
-1
@@ -10,7 +10,9 @@ import (
|
|||||||
func BenchmarkOIDCMiddleware(b *testing.B) {
|
func BenchmarkOIDCMiddleware(b *testing.B) {
|
||||||
// Setup test environment
|
// Setup test environment
|
||||||
|
|
||||||
ts := &TestSuite{}
|
// Create a testing.T wrapper for benchmarks
|
||||||
|
t := &testing.T{}
|
||||||
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
ts.token = "valid.jwt.token"
|
ts.token = "valid.jwt.token"
|
||||||
|
|
||||||
|
|||||||
+95
-95
@@ -32,10 +32,25 @@ type TestSuite struct {
|
|||||||
mockJWKCache *MockJWKCache
|
mockJWKCache *MockJWKCache
|
||||||
sessionManager *SessionManager
|
sessionManager *SessionManager
|
||||||
token string
|
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
|
// Setup initializes the test suite
|
||||||
func (ts *TestSuite) Setup() {
|
func (ts *TestSuite) Setup() {
|
||||||
|
// Initialize unified test framework if not already done
|
||||||
|
if ts.utf == nil {
|
||||||
|
ts.utf = NewUnifiedTestFramework(ts.t)
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
ts.rsaPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
ts.rsaPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,6 +107,9 @@ func (ts *TestSuite) Setup() {
|
|||||||
logger := NewLogger("info")
|
logger := NewLogger("info")
|
||||||
ts.sessionManager, _ = NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger)
|
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
|
// Common TraefikOidc instance
|
||||||
ts.tOidc = &TraefikOidc{
|
ts.tOidc = &TraefikOidc{
|
||||||
issuerURL: "https://test-issuer.com",
|
issuerURL: "https://test-issuer.com",
|
||||||
@@ -101,19 +119,23 @@ func (ts *TestSuite) Setup() {
|
|||||||
jwksURL: "https://test-jwks-url.com",
|
jwksURL: "https://test-jwks-url.com",
|
||||||
revocationURL: "https://revocation-endpoint.com",
|
revocationURL: "https://revocation-endpoint.com",
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||||
tokenBlacklist: NewCache(), // Use generic cache for blacklist
|
tokenBlacklist: ts.utf.NewCache(), // Use framework's tracked cache
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: ts.utf.NewTokenCache(), // Use framework's tracked token cache
|
||||||
logger: logger,
|
logger: logger,
|
||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
excludedURLs: map[string]struct{}{"/favicon": {}},
|
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
|
// Explicitly set paths as New() is bypassed
|
||||||
redirURLPath: "/callback", // Assume default callback path for tests
|
redirURLPath: "/callback", // Assume default callback path for tests
|
||||||
logoutURLPath: "/callback/logout", // Assume default logout path for tests
|
logoutURLPath: "/callback/logout", // Assume default logout path for tests
|
||||||
tokenURL: "https://test-issuer.com/token", // Explicitly set for refresh tests
|
tokenURL: "https://test-issuer.com/token", // Explicitly set for refresh tests
|
||||||
extractClaimsFunc: extractClaims,
|
extractClaimsFunc: extractClaims,
|
||||||
initComplete: make(chan struct{}),
|
initComplete: make(chan struct{}),
|
||||||
sessionManager: ts.sessionManager,
|
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)
|
close(ts.tOidc.initComplete)
|
||||||
// ts.tOidc.exchangeCodeForTokenFunc = ts.exchangeCodeForTokenFunc // Removed
|
// ts.tOidc.exchangeCodeForTokenFunc = ts.exchangeCodeForTokenFunc // Removed
|
||||||
@@ -139,6 +161,9 @@ func (ts *TestSuite) Setup() {
|
|||||||
return nil
|
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.
|
// 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
|
// TestVerifyToken tests the VerifyToken method
|
||||||
func TestVerifyToken(t *testing.T) {
|
func TestVerifyToken(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -361,7 +386,7 @@ func TestVerifyToken(t *testing.T) {
|
|||||||
|
|
||||||
// TestServeHTTP tests the ServeHTTP method
|
// TestServeHTTP tests the ServeHTTP method
|
||||||
func TestServeHTTP(t *testing.T) {
|
func TestServeHTTP(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -739,10 +764,8 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
ts.tOidc.tokenCache = NewTokenCache()
|
ts.tOidc.tokenCache = NewTokenCache()
|
||||||
|
|
||||||
// Reset the global replayCache to prevent "token replay detected" errors
|
// Reset the global replayCache to prevent "token replay detected" errors
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Store original tokenVerifier to restore later
|
// Store original tokenVerifier to restore later
|
||||||
origTokenVerifier := ts.tOidc.tokenVerifier
|
origTokenVerifier := ts.tOidc.tokenVerifier
|
||||||
@@ -752,10 +775,8 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
mockTokenVerifier := &MockTokenVerifier{
|
mockTokenVerifier := &MockTokenVerifier{
|
||||||
VerifyFunc: func(token string) error {
|
VerifyFunc: func(token string) error {
|
||||||
// Clear replay cache before token verification
|
// Clear replay cache before token verification
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Call the original verifier's VerifyToken method
|
// Call the original verifier's VerifyToken method
|
||||||
// Ensure origTokenVerifier is not nil and is the correct type if necessary,
|
// 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) {
|
func TestJWKToPEM(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -922,7 +943,7 @@ func TestJWKToPEM(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseJWT(t *testing.T) {
|
func TestParseJWT(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -963,7 +984,7 @@ func TestParseJWT(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestJWTVerify_MissingClaims(t *testing.T) {
|
func TestJWTVerify_MissingClaims(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
jwt := &JWT{
|
jwt := &JWT{
|
||||||
@@ -983,7 +1004,7 @@ func TestJWTVerify_MissingClaims(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleCallback(t *testing.T) {
|
func TestHandleCallback(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
redirectURL := "http://example.com/"
|
redirectURL := "http://example.com/"
|
||||||
@@ -1162,10 +1183,8 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
// Capture range variable
|
// Capture range variable
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
// Clear the global replay cache before each test run
|
// Clear the global replay cache before each test run
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Explicitly clear the shared blacklist at the start of each sub-test
|
// 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.
|
// 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) {
|
func TestIsAllowedDomain(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -1337,7 +1356,7 @@ func TestIsAllowedDomain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCHandler(t *testing.T) {
|
func TestOIDCHandler(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
ts.token = "valid.jwt.token"
|
ts.token = "valid.jwt.token"
|
||||||
@@ -1481,7 +1500,7 @@ func TestOIDCHandler(t *testing.T) {
|
|||||||
|
|
||||||
// TestHandleLogout tests the logout functionality
|
// TestHandleLogout tests the logout functionality
|
||||||
func TestHandleLogout(t *testing.T) {
|
func TestHandleLogout(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create mock revocation endpoint server
|
// Create mock revocation endpoint server
|
||||||
@@ -1652,7 +1671,7 @@ func TestHandleLogout(t *testing.T) {
|
|||||||
|
|
||||||
// TestRevokeTokenWithProvider tests the token revocation with provider
|
// TestRevokeTokenWithProvider tests the token revocation with provider
|
||||||
func TestRevokeTokenWithProvider(t *testing.T) {
|
func TestRevokeTokenWithProvider(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -1728,7 +1747,7 @@ func TestRevokeTokenWithProvider(t *testing.T) {
|
|||||||
|
|
||||||
// TestRevokeToken tests the token revocation functionality
|
// TestRevokeToken tests the token revocation functionality
|
||||||
func TestRevokeToken(t *testing.T) {
|
func TestRevokeToken(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
token := "test.token.with.claims"
|
token := "test.token.with.claims"
|
||||||
@@ -1820,7 +1839,7 @@ func TestBuildLogoutURL(t *testing.T) {
|
|||||||
|
|
||||||
// Add this new test function
|
// Add this new test function
|
||||||
func TestHandleExpiredToken(t *testing.T) {
|
func TestHandleExpiredToken(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -1928,7 +1947,7 @@ func TestHandleExpiredToken(t *testing.T) {
|
|||||||
|
|
||||||
// Add this new test function
|
// Add this new test function
|
||||||
func TestExtractGroupsAndRoles(t *testing.T) {
|
func TestExtractGroupsAndRoles(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -2094,7 +2113,7 @@ func TestMultipleMiddlewareInstances(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServeHTTPRolesAndGroups(t *testing.T) {
|
func TestServeHTTPRolesAndGroups(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create consistent timestamps for all test cases
|
// 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
|
// TestExchangeTokensWithRedirects tests the token exchange process with redirects
|
||||||
func TestExchangeTokensWithRedirects(t *testing.T) {
|
func TestExchangeTokensWithRedirects(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -2414,7 +2433,7 @@ func TestExchangeTokensWithRedirects(t *testing.T) {
|
|||||||
|
|
||||||
// TestBuildAuthURL tests the buildAuthURL function with various URL scenarios
|
// TestBuildAuthURL tests the buildAuthURL function with various URL scenarios
|
||||||
func TestBuildAuthURL(t *testing.T) {
|
func TestBuildAuthURL(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -2570,7 +2589,7 @@ func TestBuildAuthURL(t *testing.T) {
|
|||||||
|
|
||||||
// TestExchangeCodeForToken tests the exchangeCodeForToken function with PKCE support
|
// TestExchangeCodeForToken tests the exchangeCodeForToken function with PKCE support
|
||||||
func TestExchangeCodeForToken(t *testing.T) {
|
func TestExchangeCodeForToken(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -2688,7 +2707,7 @@ func TestExchangeCodeForToken(t *testing.T) {
|
|||||||
|
|
||||||
// TestDefaultInitiateAuthentication_PreservesQueryParameters tests that defaultInitiateAuthentication preserves query parameters in the incoming path.
|
// TestDefaultInitiateAuthentication_PreservesQueryParameters tests that defaultInitiateAuthentication preserves query parameters in the incoming path.
|
||||||
func TestDefaultInitiateAuthentication_PreservesQueryParameters(t *testing.T) {
|
func TestDefaultInitiateAuthentication_PreservesQueryParameters(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a request with query parameters
|
// Create a request with query parameters
|
||||||
@@ -2845,14 +2864,12 @@ func TestVerifyTimeConstraint(t *testing.T) {
|
|||||||
|
|
||||||
// TestJWTVerifyWithSkipReplayCheck tests the new skipReplayCheck parameter functionality
|
// TestJWTVerifyWithSkipReplayCheck tests the new skipReplayCheck parameter functionality
|
||||||
func TestJWTVerifyWithSkipReplayCheck(t *testing.T) {
|
func TestJWTVerifyWithSkipReplayCheck(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Clear the global replay cache before test
|
// Clear the global replay cache before test
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Create a test JWT with unique JTI
|
// Create a test JWT with unique JTI
|
||||||
jti := generateRandomString(16)
|
jti := generateRandomString(16)
|
||||||
@@ -2913,10 +2930,8 @@ func TestJWTVerifyWithSkipReplayCheck(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
if tc.firstCall {
|
if tc.firstCall {
|
||||||
// Clear replay cache for first call tests
|
// Clear replay cache for first call tests
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := jwt.Verify("https://test-issuer.com", "test-client-id", tc.skipReplayCheck)
|
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
|
// TestJWTVerifyBackwardCompatibility tests that calls without the skipReplayCheck parameter default to replay checking
|
||||||
func TestJWTVerifyBackwardCompatibility(t *testing.T) {
|
func TestJWTVerifyBackwardCompatibility(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Clear the global replay cache
|
// Clear the global replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Create a test JWT with unique JTI
|
// Create a test JWT with unique JTI
|
||||||
jti := generateRandomString(16)
|
jti := generateRandomString(16)
|
||||||
@@ -2991,14 +3004,12 @@ func TestJWTVerifyBackwardCompatibility(t *testing.T) {
|
|||||||
|
|
||||||
// TestTokenReplayDetectionFalsePositiveFix tests the specific scenario that was causing false positives
|
// TestTokenReplayDetectionFalsePositiveFix tests the specific scenario that was causing false positives
|
||||||
func TestTokenReplayDetectionFalsePositiveFix(t *testing.T) {
|
func TestTokenReplayDetectionFalsePositiveFix(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Clear the global replay cache
|
// Clear the global replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Create a test JWT with unique JTI
|
// Create a test JWT with unique JTI
|
||||||
jti := generateRandomString(16)
|
jti := generateRandomString(16)
|
||||||
@@ -3064,14 +3075,12 @@ func TestTokenReplayDetectionFalsePositiveFix(t *testing.T) {
|
|||||||
|
|
||||||
// TestAuthenticationFlowReplayDetection tests the complete authentication flow
|
// TestAuthenticationFlowReplayDetection tests the complete authentication flow
|
||||||
func TestAuthenticationFlowReplayDetection(t *testing.T) {
|
func TestAuthenticationFlowReplayDetection(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Clear the global replay cache
|
// Clear the global replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Create a test JWT with unique JTI
|
// Create a test JWT with unique JTI
|
||||||
jti := generateRandomString(16)
|
jti := generateRandomString(16)
|
||||||
@@ -3139,14 +3148,12 @@ func TestAuthenticationFlowReplayDetection(t *testing.T) {
|
|||||||
|
|
||||||
// TestActualReplayAttackDetection ensures real replay attacks are still properly detected
|
// TestActualReplayAttackDetection ensures real replay attacks are still properly detected
|
||||||
func TestActualReplayAttackDetection(t *testing.T) {
|
func TestActualReplayAttackDetection(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Clear the global replay cache
|
// Clear the global replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Create a test JWT with unique JTI
|
// Create a test JWT with unique JTI
|
||||||
jti := generateRandomString(16)
|
jti := generateRandomString(16)
|
||||||
@@ -3217,17 +3224,15 @@ func TestActualReplayAttackDetection(t *testing.T) {
|
|||||||
|
|
||||||
// TestConcurrentTokenValidation tests thread safety of replay detection
|
// TestConcurrentTokenValidation tests thread safety of replay detection
|
||||||
func TestConcurrentTokenValidation(t *testing.T) {
|
func TestConcurrentTokenValidation(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Configure rate limiter to allow more requests for concurrent testing
|
// 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
|
ts.tOidc.limiter = rate.NewLimiter(rate.Limit(1000), 1000) // Allow 1000 requests per second with burst of 1000
|
||||||
|
|
||||||
// Clear the global replay cache
|
// Clear the global replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Create multiple tokens with unique JTIs
|
// Create multiple tokens with unique JTIs
|
||||||
var tokens []string
|
var tokens []string
|
||||||
@@ -3306,17 +3311,16 @@ func TestConcurrentTokenValidation(t *testing.T) {
|
|||||||
|
|
||||||
// TestJTIBlacklistBehavior tests the JTI blacklist cache management
|
// TestJTIBlacklistBehavior tests the JTI blacklist cache management
|
||||||
func TestJTIBlacklistBehavior(t *testing.T) {
|
func TestJTIBlacklistBehavior(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Clear the global replay cache
|
// Properly reinitialize the global replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache() // Clean up any existing cache and reset sync.Once
|
||||||
replayCache = NewCache()
|
initReplayCache() // Initialize new cache through proper channel
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Create a test JWT with unique JTI
|
// Create a test JWT with unique JTI
|
||||||
jti := generateRandomString(16)
|
jti := generateRandomString(16)
|
||||||
|
t.Logf("TestJTIBlacklistBehavior - JTI: %s", jti)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
exp := now.Add(1 * time.Hour).Unix()
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
iat := now.Unix()
|
iat := now.Unix()
|
||||||
@@ -3355,8 +3359,8 @@ func TestJTIBlacklistBehavior(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "JTI exists in blacklist after verification",
|
name: "JTI exists in blacklist after verification",
|
||||||
action: func() error {
|
action: func() error {
|
||||||
replayCacheMu.Lock()
|
replayCacheMu.RLock()
|
||||||
defer replayCacheMu.Unlock()
|
defer replayCacheMu.RUnlock()
|
||||||
if _, exists := replayCache.Get(jti); !exists {
|
if _, exists := replayCache.Get(jti); !exists {
|
||||||
return fmt.Errorf("JTI not found in blacklist cache")
|
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
|
// TestSessionBasedTokenRevalidation tests token revalidation in session-based scenarios
|
||||||
func TestSessionBasedTokenRevalidation(t *testing.T) {
|
func TestSessionBasedTokenRevalidation(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Clear the global replay cache
|
// Clear the global replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
replayCache = NewCache()
|
initReplayCache()
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Create a test JWT with unique JTI
|
// Create a test JWT with unique JTI
|
||||||
jti := generateRandomString(16)
|
jti := generateRandomString(16)
|
||||||
@@ -3480,14 +3482,12 @@ func TestSessionBasedTokenRevalidation(t *testing.T) {
|
|||||||
|
|
||||||
// TestEdgeCasesWithDifferentTokenTypes tests replay detection with different token types
|
// TestEdgeCasesWithDifferentTokenTypes tests replay detection with different token types
|
||||||
func TestEdgeCasesWithDifferentTokenTypes(t *testing.T) {
|
func TestEdgeCasesWithDifferentTokenTypes(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Clear the global replay cache
|
// Properly reinitialize the global replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache() // Clean up any existing cache and reset sync.Once
|
||||||
replayCache = NewCache()
|
initReplayCache() // Initialize new cache through proper channel
|
||||||
replayCache.SetMaxSize(10000)
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
exp := now.Add(1 * time.Hour).Unix()
|
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
|
// TestBuildAuthURLWithMergedScopes tests that the auth URL includes the properly merged scopes
|
||||||
func TestBuildAuthURLWithMergedScopes(t *testing.T) {
|
func TestBuildAuthURLWithMergedScopes(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -4026,7 +4026,7 @@ func TestBuildAuthURLWithMergedScopes(t *testing.T) {
|
|||||||
// TestBuildAuthURL_OverrideScopes_And_OfflineAccess tests the offline_access logic in buildAuthURL
|
// TestBuildAuthURL_OverrideScopes_And_OfflineAccess tests the offline_access logic in buildAuthURL
|
||||||
// considering the overrideScopes flag.
|
// considering the overrideScopes flag.
|
||||||
func TestBuildAuthURL_OverrideScopes_And_OfflineAccess(t *testing.T) {
|
func TestBuildAuthURL_OverrideScopes_And_OfflineAccess(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup() // Sets up ts.tOidc
|
ts.Setup() // Sets up ts.tOidc
|
||||||
|
|
||||||
tests := []struct {
|
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.
|
// TestBuildAuthURL_SpecificUserCase tests the buildAuthURL function with the specific user-reported scenario.
|
||||||
func TestBuildAuthURL_SpecificUserCase(t *testing.T) {
|
func TestBuildAuthURL_SpecificUserCase(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup() // Basic setup for tOidc
|
ts.Setup() // Basic setup for tOidc
|
||||||
|
|
||||||
// Configure the TraefikOidc instance for the specific scenario
|
// Configure the TraefikOidc instance for the specific scenario
|
||||||
|
|||||||
+30
-33
@@ -17,9 +17,12 @@ import (
|
|||||||
|
|
||||||
// TestConcurrentTokenVerification tests race conditions in token verification
|
// TestConcurrentTokenVerification tests race conditions in token verification
|
||||||
func TestConcurrentTokenVerification(t *testing.T) {
|
func TestConcurrentTokenVerification(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
|
// Create cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
// Create multiple valid tokens to avoid replay detection
|
// Create multiple valid tokens to avoid replay detection
|
||||||
tokens := make([]string, 10)
|
tokens := make([]string, 10)
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
@@ -41,12 +44,14 @@ func TestConcurrentTokenVerification(t *testing.T) {
|
|||||||
|
|
||||||
// Create a fresh instance for this test
|
// Create a fresh instance for this test
|
||||||
var goroutineWG sync.WaitGroup
|
var goroutineWG sync.WaitGroup
|
||||||
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
tOidc := &TraefikOidc{
|
tOidc := &TraefikOidc{
|
||||||
issuerURL: "https://test-issuer.com",
|
issuerURL: "https://test-issuer.com",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
jwkCache: ts.mockJWKCache,
|
jwkCache: ts.mockJWKCache,
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: tokenBlacklist,
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: tokenCache,
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high rate limit
|
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high rate limit
|
||||||
logger: NewLogger("debug"),
|
logger: NewLogger("debug"),
|
||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
@@ -56,13 +61,7 @@ func TestConcurrentTokenVerification(t *testing.T) {
|
|||||||
}
|
}
|
||||||
tOidc.tokenVerifier = tOidc
|
tOidc.tokenVerifier = tOidc
|
||||||
tOidc.jwtVerifier = tOidc
|
tOidc.jwtVerifier = tOidc
|
||||||
|
tc.addOIDC(tOidc)
|
||||||
// Ensure cleanup when test finishes
|
|
||||||
defer func() {
|
|
||||||
if err := tOidc.Close(); err != nil {
|
|
||||||
t.Logf("Error closing TraefikOidc instance: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Test concurrent verification
|
// Test concurrent verification
|
||||||
const numGoroutines = 50
|
const numGoroutines = 50
|
||||||
@@ -132,7 +131,7 @@ func TestConcurrentTokenVerification(t *testing.T) {
|
|||||||
|
|
||||||
// TestCacheMemoryExhaustion tests cache behavior under memory pressure
|
// TestCacheMemoryExhaustion tests cache behavior under memory pressure
|
||||||
func TestCacheMemoryExhaustion(t *testing.T) {
|
func TestCacheMemoryExhaustion(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a cache with limited size
|
// Create a cache with limited size
|
||||||
@@ -374,7 +373,7 @@ func TestProviderFailureRecovery(t *testing.T) {
|
|||||||
|
|
||||||
// TestOversizedTokenHandling tests boundary value handling
|
// TestOversizedTokenHandling tests boundary value handling
|
||||||
func TestOversizedTokenHandling(t *testing.T) {
|
func TestOversizedTokenHandling(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create an oversized token with large claims
|
// Create an oversized token with large claims
|
||||||
@@ -443,7 +442,7 @@ func TestOversizedTokenHandling(t *testing.T) {
|
|||||||
|
|
||||||
// TestMaliciousInputValidation tests security input validation
|
// TestMaliciousInputValidation tests security input validation
|
||||||
func TestMaliciousInputValidation(t *testing.T) {
|
func TestMaliciousInputValidation(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
maliciousInputs := []struct {
|
maliciousInputs := []struct {
|
||||||
@@ -498,14 +497,19 @@ func TestMaliciousInputValidation(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range maliciousInputs {
|
for _, test := range maliciousInputs {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
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
|
// Create a fresh instance for each test to avoid rate limiting issues
|
||||||
var goroutineWG sync.WaitGroup
|
var goroutineWG sync.WaitGroup
|
||||||
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
freshOidc := &TraefikOidc{
|
freshOidc := &TraefikOidc{
|
||||||
issuerURL: "https://test-issuer.com",
|
issuerURL: "https://test-issuer.com",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
jwkCache: ts.mockJWKCache,
|
jwkCache: ts.mockJWKCache,
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: tokenBlacklist,
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: tokenCache,
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high rate limit
|
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high rate limit
|
||||||
logger: NewLogger("debug"),
|
logger: NewLogger("debug"),
|
||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
@@ -515,13 +519,7 @@ func TestMaliciousInputValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
freshOidc.tokenVerifier = freshOidc
|
freshOidc.tokenVerifier = freshOidc
|
||||||
freshOidc.jwtVerifier = freshOidc
|
freshOidc.jwtVerifier = freshOidc
|
||||||
|
tc.addOIDC(freshOidc)
|
||||||
// Ensure cleanup when test finishes
|
|
||||||
defer func() {
|
|
||||||
if err := freshOidc.Close(); err != nil {
|
|
||||||
t.Logf("Error closing TraefikOidc instance: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// All malicious inputs should be safely rejected
|
// All malicious inputs should be safely rejected
|
||||||
err := freshOidc.VerifyToken(test.token)
|
err := freshOidc.VerifyToken(test.token)
|
||||||
@@ -650,7 +648,7 @@ func TestResourceLimits(t *testing.T) {
|
|||||||
|
|
||||||
// TestErrorRecoveryPatterns tests various error recovery scenarios
|
// TestErrorRecoveryPatterns tests various error recovery scenarios
|
||||||
func TestErrorRecoveryPatterns(t *testing.T) {
|
func TestErrorRecoveryPatterns(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Test recovery from cache corruption
|
// Test recovery from cache corruption
|
||||||
@@ -711,9 +709,12 @@ func TestPerformanceUnderLoad(t *testing.T) {
|
|||||||
t.Skip("Skipping performance test in short mode")
|
t.Skip("Skipping performance test in short mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
|
// Create cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
// Create multiple valid tokens
|
// Create multiple valid tokens
|
||||||
const numTokens = 100
|
const numTokens = 100
|
||||||
tokens := make([]string, numTokens)
|
tokens := make([]string, numTokens)
|
||||||
@@ -736,12 +737,14 @@ func TestPerformanceUnderLoad(t *testing.T) {
|
|||||||
|
|
||||||
// Create fresh instance with high rate limit
|
// Create fresh instance with high rate limit
|
||||||
var goroutineWG sync.WaitGroup
|
var goroutineWG sync.WaitGroup
|
||||||
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
tOidc := &TraefikOidc{
|
tOidc := &TraefikOidc{
|
||||||
issuerURL: "https://test-issuer.com",
|
issuerURL: "https://test-issuer.com",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
jwkCache: ts.mockJWKCache,
|
jwkCache: ts.mockJWKCache,
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: tokenBlacklist,
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: tokenCache,
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high limit
|
limiter: rate.NewLimiter(rate.Every(time.Microsecond), 10000), // Very high limit
|
||||||
logger: NewLogger("info"), // Reduce logging for performance
|
logger: NewLogger("info"), // Reduce logging for performance
|
||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
@@ -751,13 +754,7 @@ func TestPerformanceUnderLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
tOidc.tokenVerifier = tOidc
|
tOidc.tokenVerifier = tOidc
|
||||||
tOidc.jwtVerifier = tOidc
|
tOidc.jwtVerifier = tOidc
|
||||||
|
tc.addOIDC(tOidc)
|
||||||
// Ensure cleanup when test finishes
|
|
||||||
defer func() {
|
|
||||||
if err := tOidc.Close(); err != nil {
|
|
||||||
t.Logf("Error closing TraefikOidc instance: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Performance test
|
// Performance test
|
||||||
const iterations = 1000
|
const iterations = 1000
|
||||||
|
|||||||
+1
-1
@@ -192,7 +192,7 @@ func TestScopesConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildAuthURLScopeHandling(t *testing.T) {
|
func TestBuildAuthURLScopeHandling(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup() // Basic setup for TraefikOidc instance
|
ts.Setup() // Basic setup for TraefikOidc instance
|
||||||
|
|
||||||
// Default scopes expected if not overridden and no user scopes provided
|
// 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
|
// 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)
|
// where an attacker might try to switch from an asymmetric algorithm (RS256) to a symmetric one (HS256)
|
||||||
func TestJWTAlgorithmConfusionAttack(t *testing.T) {
|
func TestJWTAlgorithmConfusionAttack(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a standard JWT with RS256 algorithm
|
// 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
|
// TestJWTNoneAlgorithmAttack tests the plugin's resistance to the "none" algorithm attack
|
||||||
// where an attacker removes the signature and sets the algorithm to "none"
|
// where an attacker removes the signature and sets the algorithm to "none"
|
||||||
func TestJWTNoneAlgorithmAttack(t *testing.T) {
|
func TestJWTNoneAlgorithmAttack(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a standard JWT
|
// 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
|
// TestJWTTokenTampering tests the plugin's ability to detect modifications to the JWT payload
|
||||||
func TestJWTTokenTampering(t *testing.T) {
|
func TestJWTTokenTampering(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a standard JWT
|
// Create a standard JWT
|
||||||
@@ -215,7 +215,7 @@ func TestJWTTokenTampering(t *testing.T) {
|
|||||||
|
|
||||||
// TestJWTExpiredToken tests the plugin's handling of expired tokens
|
// TestJWTExpiredToken tests the plugin's handling of expired tokens
|
||||||
func TestJWTExpiredToken(t *testing.T) {
|
func TestJWTExpiredToken(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a JWT that is already expired
|
// 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
|
// TestJWTFutureToken tests the plugin's handling of tokens issued in the future
|
||||||
func TestJWTFutureToken(t *testing.T) {
|
func TestJWTFutureToken(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a JWT with a future issuance time
|
// 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
|
// TestJWTReplayAttack tests the plugin's protection against token replay attacks
|
||||||
func TestJWTReplayAttack(t *testing.T) {
|
func TestJWTReplayAttack(t *testing.T) {
|
||||||
|
// Create cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
// Create a new instance for this test to avoid interference from global state
|
// Create a new instance for this test to avoid interference from global state
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
tokenBlacklist := NewCache()
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
tokenCache := NewTokenCache()
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
|
|
||||||
// Create keys
|
// Create keys
|
||||||
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
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
|
// TestMissingClaims tests validation of tokens with missing required claims
|
||||||
func TestMissingClaims(t *testing.T) {
|
func TestMissingClaims(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Test cases for missing claims
|
// Test cases for missing claims
|
||||||
@@ -460,6 +463,9 @@ func TestMissingClaims(t *testing.T) {
|
|||||||
|
|
||||||
// TestSessionFixationAttack tests the plugin's resistance to session fixation attacks
|
// TestSessionFixationAttack tests the plugin's resistance to session fixation attacks
|
||||||
func TestSessionFixationAttack(t *testing.T) {
|
func TestSessionFixationAttack(t *testing.T) {
|
||||||
|
// Create cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
sm, err := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger)
|
sm, err := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, "", logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -536,6 +542,8 @@ func TestSessionFixationAttack(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the TraefikOidc middleware
|
// Create the TraefikOidc middleware
|
||||||
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
tOidc := &TraefikOidc{
|
tOidc := &TraefikOidc{
|
||||||
next: nextHandler,
|
next: nextHandler,
|
||||||
name: "test",
|
name: "test",
|
||||||
@@ -546,8 +554,8 @@ func TestSessionFixationAttack(t *testing.T) {
|
|||||||
clientSecret: "test-client-secret",
|
clientSecret: "test-client-secret",
|
||||||
jwkCache: mockJWKCache,
|
jwkCache: mockJWKCache,
|
||||||
jwksURL: "https://test-jwks-url.com",
|
jwksURL: "https://test-jwks-url.com",
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: tokenBlacklist,
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: tokenCache,
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
@@ -798,10 +806,13 @@ func TestCSRFProtection(t *testing.T) {
|
|||||||
|
|
||||||
// TestTokenBlacklisting tests the token blacklisting mechanism
|
// TestTokenBlacklisting tests the token blacklisting mechanism
|
||||||
func TestTokenBlacklisting(t *testing.T) {
|
func TestTokenBlacklisting(t *testing.T) {
|
||||||
|
// Create cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
// Create a new instance for this test to avoid interference from global state
|
// Create a new instance for this test to avoid interference from global state
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
tokenBlacklist := NewCache()
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
tokenCache := NewTokenCache()
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
|
|
||||||
// Create keys
|
// Create keys
|
||||||
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
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
|
// TestDifferentSigningAlgorithms tests that the plugin properly handles different signing algorithms
|
||||||
func TestDifferentSigningAlgorithms(t *testing.T) {
|
func TestDifferentSigningAlgorithms(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Test cases for different algorithms - the implementation actually supports multiple algorithms
|
// 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
|
// TestMalformedTokens tests the plugin's handling of malformed tokens
|
||||||
func TestMalformedTokens(t *testing.T) {
|
func TestMalformedTokens(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -1250,23 +1261,28 @@ func TestMalformedTokens(t *testing.T) {
|
|||||||
|
|
||||||
// TestRateLimiting tests the rate limiting functionality to prevent brute force attacks
|
// TestRateLimiting tests the rate limiting functionality to prevent brute force attacks
|
||||||
func TestRateLimiting(t *testing.T) {
|
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
|
// Create a fresh instance for this test to avoid affecting other tests with rate limiting
|
||||||
logger := NewLogger("debug")
|
logger := NewLogger("debug")
|
||||||
|
|
||||||
// Create a new test suite for this test only
|
// Create a new test suite for this test only
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a separate TraefikOidc instance with a very restrictive rate limiter
|
// Create a separate TraefikOidc instance with a very restrictive rate limiter
|
||||||
// This prevents the global instance from being rate-limited
|
// This prevents the global instance from being rate-limited
|
||||||
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
tOidc := &TraefikOidc{
|
tOidc := &TraefikOidc{
|
||||||
issuerURL: "https://test-issuer.com",
|
issuerURL: "https://test-issuer.com",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
clientSecret: "test-client-secret",
|
clientSecret: "test-client-secret",
|
||||||
jwkCache: ts.mockJWKCache,
|
jwkCache: ts.mockJWKCache,
|
||||||
jwksURL: "https://test-jwks-url.com",
|
jwksURL: "https://test-jwks-url.com",
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: tokenBlacklist,
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: tokenCache,
|
||||||
// Allow only 2 requests per 10 seconds
|
// Allow only 2 requests per 10 seconds
|
||||||
limiter: rate.NewLimiter(rate.Every(10*time.Second), 2),
|
limiter: rate.NewLimiter(rate.Every(10*time.Second), 2),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -1347,7 +1363,10 @@ func TestRateLimiting(t *testing.T) {
|
|||||||
// TestAuthorizationHeaderBypass tests that the plugin correctly handles attempts to bypass
|
// TestAuthorizationHeaderBypass tests that the plugin correctly handles attempts to bypass
|
||||||
// authorization by directly providing an Authorization header
|
// authorization by directly providing an Authorization header
|
||||||
func TestAuthorizationHeaderBypass(t *testing.T) {
|
func TestAuthorizationHeaderBypass(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
// Create cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a test next handler that would indicate successful authentication
|
// Create a test next handler that would indicate successful authentication
|
||||||
@@ -1357,6 +1376,8 @@ func TestAuthorizationHeaderBypass(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Create the TraefikOidc instance
|
// Create the TraefikOidc instance
|
||||||
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
tOidc := &TraefikOidc{
|
tOidc := &TraefikOidc{
|
||||||
next: nextHandler,
|
next: nextHandler,
|
||||||
name: "test",
|
name: "test",
|
||||||
@@ -1367,8 +1388,8 @@ func TestAuthorizationHeaderBypass(t *testing.T) {
|
|||||||
clientSecret: "test-client-secret",
|
clientSecret: "test-client-secret",
|
||||||
jwkCache: ts.mockJWKCache,
|
jwkCache: ts.mockJWKCache,
|
||||||
jwksURL: "https://test-jwks-url.com",
|
jwksURL: "https://test-jwks-url.com",
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: tokenBlacklist,
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: tokenCache,
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||||
logger: NewLogger("debug"),
|
logger: NewLogger("debug"),
|
||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
@@ -1419,7 +1440,7 @@ func TestAuthorizationHeaderBypass(t *testing.T) {
|
|||||||
|
|
||||||
// TestEmptyAudience tests tokens with empty audience claim
|
// TestEmptyAudience tests tokens with empty audience claim
|
||||||
func TestEmptyAudience(t *testing.T) {
|
func TestEmptyAudience(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a JWT with empty audience
|
// Create a JWT with empty audience
|
||||||
@@ -1452,7 +1473,7 @@ func TestEmptyAudience(t *testing.T) {
|
|||||||
|
|
||||||
// TestEmptyIssuer tests tokens with empty issuer claim
|
// TestEmptyIssuer tests tokens with empty issuer claim
|
||||||
func TestEmptyIssuer(t *testing.T) {
|
func TestEmptyIssuer(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a JWT with empty issuer
|
// 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
|
// TestInvalidRedirectURI tests the plugin's handling of invalid redirect URIs
|
||||||
func TestInvalidRedirectURI(t *testing.T) {
|
func TestInvalidRedirectURI(t *testing.T) {
|
||||||
ts := &TestSuite{t: t}
|
// Create cleanup helper
|
||||||
|
tc := newTestCleanup(t)
|
||||||
|
|
||||||
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create a test request with an invalid redirect URI
|
// Create a test request with an invalid redirect URI
|
||||||
@@ -1527,6 +1551,8 @@ func TestInvalidRedirectURI(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Create the TraefikOidc instance
|
// Create the TraefikOidc instance
|
||||||
|
tokenBlacklist := tc.addCache(NewCache())
|
||||||
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
tOidc := &TraefikOidc{
|
tOidc := &TraefikOidc{
|
||||||
next: nextHandler,
|
next: nextHandler,
|
||||||
name: "test",
|
name: "test",
|
||||||
@@ -1537,8 +1563,8 @@ func TestInvalidRedirectURI(t *testing.T) {
|
|||||||
clientSecret: "test-client-secret",
|
clientSecret: "test-client-secret",
|
||||||
jwkCache: ts.mockJWKCache,
|
jwkCache: ts.mockJWKCache,
|
||||||
jwksURL: "https://test-jwks-url.com",
|
jwksURL: "https://test-jwks-url.com",
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: tokenBlacklist,
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: tokenCache,
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||||
logger: NewLogger("debug"),
|
logger: NewLogger("debug"),
|
||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ type SecurityMonitor struct {
|
|||||||
eventHandlers []SecurityEventHandler
|
eventHandlers []SecurityEventHandler
|
||||||
config SecurityMonitorConfig
|
config SecurityMonitorConfig
|
||||||
ipMutex sync.RWMutex
|
ipMutex sync.RWMutex
|
||||||
|
cleanupTask *BackgroundTask // Store cleanup task per instance
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPFailureTracker maintains failure statistics for a specific IP address.
|
// 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
|
// NewSecurityMonitor creates a new security monitor instance
|
||||||
func NewSecurityMonitor(config SecurityMonitorConfig, logger *Logger) *SecurityMonitor {
|
func NewSecurityMonitor(config SecurityMonitorConfig, logger *Logger) *SecurityMonitor {
|
||||||
sm := &SecurityMonitor{
|
sm := &SecurityMonitor{
|
||||||
@@ -489,19 +487,19 @@ func (spd *SuspiciousPatternDetector) DetectSuspiciousPatterns() []string {
|
|||||||
// startCleanupRoutine starts the background cleanup routine
|
// startCleanupRoutine starts the background cleanup routine
|
||||||
func (sm *SecurityMonitor) startCleanupRoutine() {
|
func (sm *SecurityMonitor) startCleanupRoutine() {
|
||||||
// Use BackgroundTask abstraction for consistent management
|
// Use BackgroundTask abstraction for consistent management
|
||||||
cleanupTask = NewBackgroundTask(
|
sm.cleanupTask = NewBackgroundTask(
|
||||||
"security-monitor-cleanup",
|
"security-monitor-cleanup",
|
||||||
time.Duration(sm.config.CleanupIntervalMinutes)*time.Minute,
|
time.Duration(sm.config.CleanupIntervalMinutes)*time.Minute,
|
||||||
sm.cleanup,
|
sm.cleanup,
|
||||||
sm.logger)
|
sm.logger)
|
||||||
cleanupTask.Start()
|
sm.cleanupTask.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopCleanupRoutine stops the background cleanup routine
|
// StopCleanupRoutine stops the background cleanup routine
|
||||||
func (sm *SecurityMonitor) StopCleanupRoutine() {
|
func (sm *SecurityMonitor) StopCleanupRoutine() {
|
||||||
if cleanupTask != nil {
|
if sm.cleanupTask != nil {
|
||||||
cleanupTask.Stop()
|
sm.cleanupTask.Stop()
|
||||||
cleanupTask = nil
|
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
|
// Mark cleanup as done for this session manager instance
|
||||||
if !sm.cleanupDone && len(processedCookies) > 0 {
|
if len(processedCookies) > 0 {
|
||||||
sm.cleanupMutex.Lock()
|
sm.cleanupMutex.Lock()
|
||||||
sm.cleanupDone = true
|
if !sm.cleanupDone {
|
||||||
|
sm.cleanupDone = true
|
||||||
|
}
|
||||||
sm.cleanupMutex.Unlock()
|
sm.cleanupMutex.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,6 +252,13 @@ func TestTemplateIntegrationWithPlugin(t *testing.T) {
|
|||||||
traefikOidc, ok := handler.(*TraefikOidc)
|
traefikOidc, ok := handler.(*TraefikOidc)
|
||||||
require.True(t, ok)
|
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
|
// Create a mock session with claims
|
||||||
req := httptest.NewRequest("GET", "/protected", nil)
|
req := httptest.NewRequest("GET", "/protected", nil)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
// in the actual middleware flow
|
// in the actual middleware flow
|
||||||
func TestTemplatedHeadersIntegration(t *testing.T) {
|
func TestTemplatedHeadersIntegration(t *testing.T) {
|
||||||
// Create a TestSuite to use its helper methods and fields
|
// Create a TestSuite to use its helper methods and fields
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -423,7 +423,7 @@ func TestTemplatedHeadersIntegration(t *testing.T) {
|
|||||||
// TestEdgeCaseTemplatedHeaders tests edge cases for templated headers
|
// TestEdgeCaseTemplatedHeaders tests edge cases for templated headers
|
||||||
func TestEdgeCaseTemplatedHeaders(t *testing.T) {
|
func TestEdgeCaseTemplatedHeaders(t *testing.T) {
|
||||||
// Create a TestSuite to use its helper methods and fields
|
// Create a TestSuite to use its helper methods and fields
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
@@ -102,6 +102,13 @@ func TestTraefikConfigurationParsing(t *testing.T) {
|
|||||||
t.Fatalf("Handler is not of type *TraefikOidc")
|
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
|
// Check that templates were parsed correctly
|
||||||
if len(tc.config.Headers) > 0 {
|
if len(tc.config.Headers) > 0 {
|
||||||
if len(middleware.headerTemplates) != len(tc.config.Headers) {
|
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")
|
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
|
// Check that the header template was parsed
|
||||||
if len(middleware.headerTemplates) != 1 {
|
if len(middleware.headerTemplates) != 1 {
|
||||||
t.Errorf("Expected 1 header template, got %d", len(middleware.headerTemplates))
|
t.Errorf("Expected 1 header template, got %d", len(middleware.headerTemplates))
|
||||||
|
|||||||
@@ -37,14 +37,8 @@ func resetGlobalState() {
|
|||||||
cacheManagerOnce = sync.Once{}
|
cacheManagerOnce = sync.Once{}
|
||||||
cacheManagerMutex.Unlock()
|
cacheManagerMutex.Unlock()
|
||||||
|
|
||||||
// Reset replay cache
|
// Reset and cleanup replay cache
|
||||||
replayCacheMu.Lock()
|
cleanupReplayCache()
|
||||||
if replayCache != nil {
|
|
||||||
replayCache.Close()
|
|
||||||
replayCache = nil
|
|
||||||
}
|
|
||||||
replayCacheOnce = sync.Once{}
|
|
||||||
replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Reset memory pools
|
// Reset memory pools
|
||||||
memoryPoolMutex.Lock()
|
memoryPoolMutex.Lock()
|
||||||
@@ -53,6 +47,104 @@ func resetGlobalState() {
|
|||||||
memoryPoolMutex.Unlock()
|
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
|
// createTestConfig creates a config with all required fields populated for testing
|
||||||
func createTestConfig() *Config {
|
func createTestConfig() *Config {
|
||||||
config := CreateConfig()
|
config := CreateConfig()
|
||||||
@@ -88,7 +88,7 @@ func TestTokenTypeDistinction(t *testing.T) {
|
|||||||
// TestTokenTypeIntegration tests the integration of ID and access tokens with the middleware
|
// TestTokenTypeIntegration tests the integration of ID and access tokens with the middleware
|
||||||
func TestTokenTypeIntegration(t *testing.T) {
|
func TestTokenTypeIntegration(t *testing.T) {
|
||||||
// Create a TestSuite to use its helper methods and fields
|
// Create a TestSuite to use its helper methods and fields
|
||||||
ts := &TestSuite{t: t}
|
ts := NewTestSuite(t)
|
||||||
ts.Setup()
|
ts.Setup()
|
||||||
|
|
||||||
// Create different tokens for ID and access tokens
|
// Create different tokens for ID and access tokens
|
||||||
@@ -157,8 +157,8 @@ func TestTokenTypeIntegration(t *testing.T) {
|
|||||||
clientSecret: "test-client-secret",
|
clientSecret: "test-client-secret",
|
||||||
jwkCache: ts.mockJWKCache,
|
jwkCache: ts.mockJWKCache,
|
||||||
jwksURL: "https://test-jwks-url.com",
|
jwksURL: "https://test-jwks-url.com",
|
||||||
tokenBlacklist: NewCache(),
|
tokenBlacklist: ts.tOidc.tokenBlacklist,
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: ts.tOidc.tokenCache,
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
||||||
logger: NewLogger("debug"),
|
logger: NewLogger("debug"),
|
||||||
allowedUserDomains: map[string]struct{}{"example.com": {}},
|
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