mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
7816e05c98
* fix(logout): handle logout requests before OIDC initialization - [x] Add debug logging to logout handler entry point - [x] Move logout path check before OIDC initialization to enable logout when provider unavailable - [x] Move excluded URL and SSE checks before initialization wait - [x] Add debug logging for initialization wait to diagnose hanging requests - [x] Add test for logout functionality without OIDC provider availability * feat(logout): implement OIDC backchannel and front-channel logout - [x] Add logout token validation and backchannel logout handler - [x] Add front-channel logout handler with iframe support - [x] Implement session invalidation cache for distributed deployments - [x] Add comprehensive logout token claim verification (issuer, audience, events, iat, sid/sub) - [x] Integrate session invalidation checks into authorization flow - [x] Add configuration options for enabling backchannel/front-channel logout - [x] Add extensive test coverage for logout flows and edge cases - [x] Update documentation with logout configuration examples - [x] Add middleware routing for logout endpoints - [x] Extend cache manager with session invalidation cache support Resolves #110 * fixup! feat(logout): implement OIDC backchannel and front-channel logout * fixup! Merge branch 'main' into fix-issue-with-logout-url
196 lines
5.7 KiB
Go
196 lines
5.7 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultBlacklistDuration = 24 * time.Hour
|
|
)
|
|
|
|
// CacheManager manages all caching components using the universal cache
|
|
type CacheManager struct {
|
|
manager *UniversalCacheManager
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
var (
|
|
globalCacheManagerInstance *CacheManager
|
|
cacheManagerInitOnce sync.Once
|
|
)
|
|
|
|
// GetGlobalCacheManager returns a singleton CacheManager instance
|
|
// Deprecated: Use GetGlobalCacheManagerWithConfig instead
|
|
func GetGlobalCacheManager(wg *sync.WaitGroup) *CacheManager {
|
|
return GetGlobalCacheManagerWithConfig(wg, nil)
|
|
}
|
|
|
|
// GetGlobalCacheManagerWithConfig returns a singleton CacheManager instance with optional Redis configuration
|
|
func GetGlobalCacheManagerWithConfig(wg *sync.WaitGroup, config *Config) *CacheManager {
|
|
cacheManagerInitOnce.Do(func() {
|
|
var redisConfig *RedisConfig
|
|
var logger *Logger
|
|
|
|
if config != nil {
|
|
logger = NewLogger(config.LogLevel)
|
|
|
|
// Initialize Redis config if not present
|
|
if config.Redis == nil {
|
|
config.Redis = &RedisConfig{}
|
|
}
|
|
|
|
// Apply environment variable fallbacks for fields not set in config
|
|
// This allows env vars to be used as optional overrides
|
|
config.Redis.ApplyEnvFallbacks()
|
|
|
|
// Apply defaults after env fallbacks
|
|
config.Redis.ApplyDefaults()
|
|
|
|
redisConfig = config.Redis
|
|
}
|
|
|
|
globalCacheManagerInstance = &CacheManager{
|
|
manager: GetUniversalCacheManagerWithConfig(logger, redisConfig),
|
|
}
|
|
})
|
|
return globalCacheManagerInstance
|
|
}
|
|
|
|
// GetSharedTokenBlacklist returns the shared token blacklist cache
|
|
func (cm *CacheManager) GetSharedTokenBlacklist() CacheInterface {
|
|
cm.mu.RLock()
|
|
defer cm.mu.RUnlock()
|
|
return &CacheInterfaceWrapper{cache: cm.manager.GetBlacklistCache(), managed: true}
|
|
}
|
|
|
|
// GetSharedTokenCache returns the shared token cache
|
|
func (cm *CacheManager) GetSharedTokenCache() *TokenCache {
|
|
cm.mu.RLock()
|
|
defer cm.mu.RUnlock()
|
|
return &TokenCache{cache: cm.manager.GetTokenCache()}
|
|
}
|
|
|
|
// GetSharedMetadataCache returns the shared metadata cache
|
|
func (cm *CacheManager) GetSharedMetadataCache() *MetadataCache {
|
|
cm.mu.RLock()
|
|
defer cm.mu.RUnlock()
|
|
return &MetadataCache{
|
|
cache: cm.manager.GetMetadataCache(),
|
|
logger: cm.manager.logger,
|
|
}
|
|
}
|
|
|
|
// GetSharedJWKCache returns the shared JWK cache
|
|
func (cm *CacheManager) GetSharedJWKCache() JWKCacheInterface {
|
|
cm.mu.RLock()
|
|
defer cm.mu.RUnlock()
|
|
return &JWKCache{cache: cm.manager.GetJWKCache()}
|
|
}
|
|
|
|
// GetSharedIntrospectionCache returns the shared token introspection cache
|
|
// for caching OAuth 2.0 Token Introspection (RFC 7662) results
|
|
func (cm *CacheManager) GetSharedIntrospectionCache() CacheInterface {
|
|
cm.mu.RLock()
|
|
defer cm.mu.RUnlock()
|
|
return &CacheInterfaceWrapper{cache: cm.manager.GetIntrospectionCache(), managed: true}
|
|
}
|
|
|
|
// GetSharedTokenTypeCache returns the shared token type cache
|
|
// for caching token type detection results to improve performance
|
|
func (cm *CacheManager) GetSharedTokenTypeCache() CacheInterface {
|
|
cm.mu.RLock()
|
|
defer cm.mu.RUnlock()
|
|
return &CacheInterfaceWrapper{cache: cm.manager.GetTokenTypeCache(), managed: true}
|
|
}
|
|
|
|
// GetSharedSessionInvalidationCache returns the shared session invalidation cache
|
|
// for backchannel and front-channel logout (IdP-initiated logout)
|
|
func (cm *CacheManager) GetSharedSessionInvalidationCache() CacheInterface {
|
|
cm.mu.RLock()
|
|
defer cm.mu.RUnlock()
|
|
return &CacheInterfaceWrapper{cache: cm.manager.GetSessionInvalidationCache(), managed: true}
|
|
}
|
|
|
|
// Close gracefully shuts down all cache components
|
|
func (cm *CacheManager) Close() error {
|
|
cm.mu.Lock()
|
|
defer cm.mu.Unlock()
|
|
return cm.manager.Close()
|
|
}
|
|
|
|
// CleanupGlobalCacheManager cleans up the global cache manager
|
|
func CleanupGlobalCacheManager() error {
|
|
if globalCacheManagerInstance != nil {
|
|
return globalCacheManagerInstance.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CacheInterfaceWrapper wraps UniversalCache to implement CacheInterface
|
|
type CacheInterfaceWrapper struct {
|
|
cache *UniversalCache
|
|
managed bool // If true, cache is managed globally and Close() is a no-op
|
|
}
|
|
|
|
// Set stores a value
|
|
func (c *CacheInterfaceWrapper) Set(key string, value interface{}, ttl time.Duration) {
|
|
_ = c.cache.Set(key, value, ttl) // Safe to ignore: cache set failures are non-critical
|
|
}
|
|
|
|
// Get retrieves a value
|
|
func (c *CacheInterfaceWrapper) Get(key string) (interface{}, bool) {
|
|
return c.cache.Get(key)
|
|
}
|
|
|
|
// Delete removes a key
|
|
func (c *CacheInterfaceWrapper) Delete(key string) {
|
|
c.cache.Delete(key)
|
|
}
|
|
|
|
// SetMaxSize updates the max size
|
|
func (c *CacheInterfaceWrapper) SetMaxSize(size int) {
|
|
c.cache.SetMaxSize(size)
|
|
}
|
|
|
|
// Cleanup triggers immediate cleanup of expired items
|
|
func (c *CacheInterfaceWrapper) Cleanup() {
|
|
c.cache.Cleanup()
|
|
}
|
|
|
|
// Close shuts down the cache if it's not managed globally.
|
|
// For managed caches (from UniversalCacheManager), this is a no-op to prevent log flooding
|
|
// when multiple plugin instances are closed during Traefik configuration reloads.
|
|
func (c *CacheInterfaceWrapper) Close() {
|
|
if c.managed {
|
|
// Cache is managed globally by UniversalCacheManager, so we don't close it here.
|
|
return
|
|
}
|
|
// Standalone cache - close it properly to stop cleanup goroutines
|
|
if c.cache != nil {
|
|
_ = c.cache.Close() // Safe to ignore: closing cache is best-effort during shutdown
|
|
}
|
|
}
|
|
|
|
// Size returns the number of items
|
|
func (c *CacheInterfaceWrapper) Size() int {
|
|
return c.cache.Size()
|
|
}
|
|
|
|
// Clear removes all items
|
|
func (c *CacheInterfaceWrapper) Clear() {
|
|
c.cache.Clear()
|
|
}
|
|
|
|
// GetStats returns cache statistics
|
|
func (c *CacheInterfaceWrapper) GetStats() map[string]interface{} {
|
|
return c.cache.GetMetrics()
|
|
}
|
|
|
|
// SetMaxMemory sets the maximum memory limit
|
|
func (c *CacheInterfaceWrapper) SetMaxMemory(bytes int64) {
|
|
c.cache.mu.Lock()
|
|
defer c.cache.mu.Unlock()
|
|
c.cache.config.MaxMemoryBytes = bytes
|
|
}
|