mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-13 04:07:37 +00:00
Complete rebuild of the plugin
* Fix bug affecting Azure OIDC authentication ( and most likely others ) * Fixes issue #51 * Ensure that appended roles are unique. Update the documentation. * Improvements targetting possible memory usage spikes. * Additional fixes and cleanup * Refactoring code to fix the issues identified by the users. * Modernize run * Fieldalignment * Multiple changes to improve performance and reduce complexity. - Optimise the errors and recovery. - Deduplicate code in metadata cache. - Remove unused performance monitoring code. - Simplify session management and settings handling. * Fix claims issue. * Add ability to overwrite the default scopes in the settings file * Well.. that escalated quickly. Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ). * Bugfix #51: Ensures that user provided scopes overrides work. * fixup! Bugfix #51: Ensures that user provided scopes overrides work. * fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work. * Abstract the provider logic into a separate package. * Additional micro fixes and cleanups. * Simplify all the things. * fixup! Simplify all the things. * fixup! fixup! Simplify all the things. * fixup! fixup! fixup! Simplify all the things. * fixup! fixup! fixup! fixup! Simplify all the things. * ... * Cleanup tests. * fixup! Cleanup tests. * fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! fixup! Cleanup tests. * Issue #53: Fix CSRF token handling in reverse proxy 1. ✅ HTTPS Detection Fixed (session.go:723) - Now uses X-Forwarded-Proto header instead of r.URL.Scheme - Properly detects HTTPS in reverse proxy environments 2. ✅ SameSite Cookie Attribute Fixed - Removed automatic SameSiteStrictMode for HTTPS (would break OAuth) - Keeps SameSiteLaxMode to allow OAuth callbacks from external domains - Only uses Strict for AJAX requests which don't involve OAuth redirects 3. ✅ Cookie Domain Handling Fixed - Now respects X-Forwarded-Host header for cookie domain - Ensures cookies are set for the public domain, not internal proxy domain 4. ✅ EnhanceSessionSecurity Properly Integrated - Function is now actually called during session save - Applies security enhancements without breaking OAuth flow Why Issue #53 Failed Before: 1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back) 2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail 3. Cookie domain might have been wrong (internal vs public domain) Why It Works Now: 1. Cookies are properly marked Secure for HTTPS 2. Uses SameSite=Lax to allow OAuth provider callbacks 3. Cookie domain uses public domain from X-Forwarded-Host 4. CSRF token persists through the entire OAuth flow * Next set of enhancements together with memory usage improvements. * Memory leak fixes and optimisations. * CSRF and Cookie Domain fixes * fixup! CSRF and Cookie Domain fixes * Metadata cache leak fix + profiling * fixup! Metadata cache leak fix + profiling * Memory leaks hunting, part 1337. * Further pursue of perfection. * fixup! Further pursue of perfection. * fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * Clear race conditions * fixup! Clear race conditions * Weekend fun with memory leaks * Splitting code into multiple files with reasonable testing coverage. ``` ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements ```` * fixup! Splitting code into multiple files with reasonable testing coverage. * fixup! fixup! Splitting code into multiple files with reasonable testing coverage. * Weekend fun with further optimisations. * fixup! Weekend fun with further optimisations. * fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * Pre-release cleanup. * Enhance test coverage. * fixup! Enhance test coverage. * fixup! fixup! Enhance test coverage. * fixup! fixup! fixup! Enhance test coverage.
This commit is contained in:
Vendored
+426
@@ -0,0 +1,426 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Type defines the type of cache for optimized behavior
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypeToken Type = "token"
|
||||
TypeMetadata Type = "metadata"
|
||||
TypeJWK Type = "jwk"
|
||||
TypeSession Type = "session"
|
||||
TypeGeneral Type = "general"
|
||||
)
|
||||
|
||||
// Logger interface for cache operations
|
||||
type Logger interface {
|
||||
Debug(msg string)
|
||||
Debugf(format string, args ...interface{})
|
||||
Info(msg string)
|
||||
Infof(format string, args ...interface{})
|
||||
Error(msg string)
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// Config provides configuration for the cache
|
||||
type Config struct {
|
||||
Type Type
|
||||
MaxSize int
|
||||
MaxMemoryBytes int64
|
||||
DefaultTTL time.Duration
|
||||
CleanupInterval time.Duration
|
||||
EnableCompression bool
|
||||
EnableMetrics bool
|
||||
EnableAutoCleanup bool
|
||||
EnableMemoryLimit bool
|
||||
Logger Logger
|
||||
|
||||
// Type-specific configurations
|
||||
TokenConfig *TokenConfig
|
||||
MetadataConfig *MetadataConfig
|
||||
JWKConfig *JWKConfig
|
||||
}
|
||||
|
||||
// TokenConfig provides token-specific cache configuration
|
||||
type TokenConfig struct {
|
||||
BlacklistTTL time.Duration
|
||||
RefreshTokenTTL time.Duration
|
||||
EnableTokenRotation bool
|
||||
}
|
||||
|
||||
// MetadataConfig provides metadata-specific cache configuration
|
||||
type MetadataConfig struct {
|
||||
GracePeriod time.Duration
|
||||
ExtendedGracePeriod time.Duration
|
||||
MaxGracePeriod time.Duration
|
||||
SecurityCriticalMaxGracePeriod time.Duration
|
||||
SecurityCriticalFields []string
|
||||
}
|
||||
|
||||
// JWKConfig provides JWK-specific cache configuration
|
||||
type JWKConfig struct {
|
||||
RefreshInterval time.Duration
|
||||
MinRefreshTime time.Duration
|
||||
MaxKeyAge time.Duration
|
||||
}
|
||||
|
||||
// Item represents a single cache entry
|
||||
type Item struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
Size int64
|
||||
ExpiresAt time.Time
|
||||
LastAccessed time.Time
|
||||
AccessCount int64
|
||||
CacheType Type
|
||||
|
||||
// Type-specific metadata
|
||||
Metadata map[string]interface{}
|
||||
|
||||
// LRU list element reference
|
||||
element *list.Element
|
||||
}
|
||||
|
||||
// Cache provides a single, unified cache implementation
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]*Item
|
||||
lruList *list.List
|
||||
config Config
|
||||
logger Logger
|
||||
|
||||
// Memory management
|
||||
currentSize int64
|
||||
currentMemory int64
|
||||
|
||||
// Metrics
|
||||
hits int64
|
||||
misses int64
|
||||
evictions int64
|
||||
sets int64
|
||||
|
||||
// Lifecycle management
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
stopCleanup chan bool
|
||||
closed int32
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default cache configuration
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Type: TypeGeneral,
|
||||
MaxSize: 1000,
|
||||
MaxMemoryBytes: 64 * 1024 * 1024, // 64MB
|
||||
DefaultTTL: 10 * time.Minute,
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
EnableAutoCleanup: true,
|
||||
EnableMemoryLimit: true,
|
||||
EnableMetrics: true,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new cache instance
|
||||
func New(config Config) *Cache {
|
||||
if config.Logger == nil {
|
||||
config.Logger = &noOpLogger{}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &Cache{
|
||||
items: make(map[string]*Item),
|
||||
lruList: list.New(),
|
||||
config: config,
|
||||
logger: config.Logger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
if config.EnableAutoCleanup && config.CleanupInterval > 0 {
|
||||
c.stopCleanup = make(chan bool)
|
||||
c.startCleanupRoutine()
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Set stores a value with TTL
|
||||
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) error {
|
||||
if atomic.LoadInt32(&c.closed) == 1 {
|
||||
return fmt.Errorf("cache is closed")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Calculate size
|
||||
size := c.estimateSize(value)
|
||||
|
||||
// Check memory limit
|
||||
if c.config.EnableMemoryLimit && c.currentMemory+size > c.config.MaxMemoryBytes {
|
||||
c.evictLRU()
|
||||
}
|
||||
|
||||
// Check size limit
|
||||
if c.config.MaxSize > 0 && len(c.items) >= c.config.MaxSize {
|
||||
c.evictLRU()
|
||||
}
|
||||
|
||||
// Create or update item
|
||||
item := &Item{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Size: size,
|
||||
ExpiresAt: time.Now().Add(ttl),
|
||||
LastAccessed: time.Now(),
|
||||
AccessCount: 0,
|
||||
CacheType: c.config.Type,
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// Remove old item if exists
|
||||
if oldItem, exists := c.items[key]; exists {
|
||||
c.lruList.Remove(oldItem.element)
|
||||
c.currentMemory -= oldItem.Size
|
||||
c.currentSize--
|
||||
}
|
||||
|
||||
// Add new item
|
||||
item.element = c.lruList.PushFront(item)
|
||||
c.items[key] = item
|
||||
c.currentMemory += size
|
||||
c.currentSize++
|
||||
atomic.AddInt64(&c.sets, 1)
|
||||
|
||||
c.logger.Debugf("Cache: Set key=%s, size=%d, ttl=%v", key, size, ttl)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a value from cache
|
||||
func (c *Cache) Get(key string) (interface{}, bool) {
|
||||
if atomic.LoadInt32(&c.closed) == 1 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
item, exists := c.items[key]
|
||||
if !exists {
|
||||
atomic.AddInt64(&c.misses, 1)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if time.Now().After(item.ExpiresAt) {
|
||||
c.removeItem(key, item)
|
||||
atomic.AddInt64(&c.misses, 1)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Update LRU
|
||||
c.lruList.MoveToFront(item.element)
|
||||
item.LastAccessed = time.Now()
|
||||
item.AccessCount++
|
||||
atomic.AddInt64(&c.hits, 1)
|
||||
|
||||
return item.Value, true
|
||||
}
|
||||
|
||||
// Delete removes a key from cache
|
||||
func (c *Cache) Delete(key string) {
|
||||
if atomic.LoadInt32(&c.closed) == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if item, exists := c.items[key]; exists {
|
||||
c.removeItem(key, item)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear removes all items from cache
|
||||
func (c *Cache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.items = make(map[string]*Item)
|
||||
c.lruList.Init()
|
||||
c.currentSize = 0
|
||||
c.currentMemory = 0
|
||||
}
|
||||
|
||||
// Size returns the number of items in cache
|
||||
func (c *Cache) Size() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.items)
|
||||
}
|
||||
|
||||
// SetMaxSize updates the maximum cache size
|
||||
func (c *Cache) SetMaxSize(size int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.config.MaxSize = size
|
||||
|
||||
// Evict items if necessary
|
||||
for len(c.items) > size && c.lruList.Len() > 0 {
|
||||
c.evictLRU()
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats returns cache statistics
|
||||
func (c *Cache) GetStats() map[string]interface{} {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"size": c.currentSize,
|
||||
"memory": c.currentMemory,
|
||||
"hits": atomic.LoadInt64(&c.hits),
|
||||
"misses": atomic.LoadInt64(&c.misses),
|
||||
"evictions": atomic.LoadInt64(&c.evictions),
|
||||
"sets": atomic.LoadInt64(&c.sets),
|
||||
"hit_rate": c.calculateHitRate(),
|
||||
"cache_type": string(c.config.Type),
|
||||
}
|
||||
}
|
||||
|
||||
// Close gracefully shuts down the cache
|
||||
func (c *Cache) Close() error {
|
||||
if !atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
|
||||
return fmt.Errorf("cache already closed")
|
||||
}
|
||||
|
||||
c.cancel()
|
||||
if c.config.EnableAutoCleanup {
|
||||
close(c.stopCleanup)
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
// Clear inline to avoid double locking
|
||||
c.items = make(map[string]*Item)
|
||||
c.lruList.Init()
|
||||
c.currentSize = 0
|
||||
c.currentMemory = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup removes expired items
|
||||
func (c *Cache) Cleanup() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
var toRemove []string
|
||||
|
||||
for key, item := range c.items {
|
||||
if now.After(item.ExpiresAt) {
|
||||
toRemove = append(toRemove, key)
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range toRemove {
|
||||
if item, exists := c.items[key]; exists {
|
||||
c.removeItem(key, item)
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Debugf("Cache cleanup: removed %d expired items", len(toRemove))
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
func (c *Cache) removeItem(key string, item *Item) {
|
||||
c.lruList.Remove(item.element)
|
||||
delete(c.items, key)
|
||||
c.currentMemory -= item.Size
|
||||
c.currentSize--
|
||||
}
|
||||
|
||||
func (c *Cache) evictLRU() {
|
||||
if elem := c.lruList.Back(); elem != nil {
|
||||
item := elem.Value.(*Item)
|
||||
c.removeItem(item.Key, item)
|
||||
atomic.AddInt64(&c.evictions, 1)
|
||||
c.logger.Debugf("Cache: Evicted LRU item key=%s", item.Key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) estimateSize(value interface{}) int64 {
|
||||
// Simple size estimation
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return int64(len(v))
|
||||
case []byte:
|
||||
return int64(len(v))
|
||||
case map[string]interface{}:
|
||||
// Rough estimation for maps
|
||||
data, _ := json.Marshal(v)
|
||||
return int64(len(data))
|
||||
default:
|
||||
// Default size for unknown types
|
||||
return 256
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) calculateHitRate() float64 {
|
||||
hits := atomic.LoadInt64(&c.hits)
|
||||
misses := atomic.LoadInt64(&c.misses)
|
||||
total := hits + misses
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(hits) / float64(total)
|
||||
}
|
||||
|
||||
func (c *Cache) startCleanupRoutine() {
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
ticker := time.NewTicker(c.config.CleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.Cleanup()
|
||||
case <-c.stopCleanup:
|
||||
return
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// noOpLogger provides a no-op logger implementation
|
||||
type noOpLogger struct{}
|
||||
|
||||
func (l *noOpLogger) Debug(msg string) {}
|
||||
func (l *noOpLogger) Debugf(format string, args ...interface{}) {}
|
||||
func (l *noOpLogger) Info(msg string) {}
|
||||
func (l *noOpLogger) Infof(format string, args ...interface{}) {}
|
||||
func (l *noOpLogger) Error(msg string) {}
|
||||
func (l *noOpLogger) Errorf(format string, args ...interface{}) {}
|
||||
func (l *noOpLogger) Warn(msg string) {}
|
||||
func (l *noOpLogger) Warnf(format string, args ...interface{}) {}
|
||||
func (l *noOpLogger) Fatal(msg string) {}
|
||||
func (l *noOpLogger) Fatalf(format string, args ...interface{}) {}
|
||||
func (l *noOpLogger) WithField(key string, value interface{}) Logger { return l }
|
||||
func (l *noOpLogger) WithFields(fields map[string]interface{}) Logger { return l }
|
||||
Vendored
+2040
File diff suppressed because it is too large
Load Diff
Vendored
+278
@@ -0,0 +1,278 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CompatibilityWrapper provides backward compatibility with existing cache interfaces
|
||||
type CompatibilityWrapper struct {
|
||||
cache *Cache
|
||||
}
|
||||
|
||||
// NewCompatibilityWrapper creates a new compatibility wrapper
|
||||
func NewCompatibilityWrapper(cache *Cache) *CompatibilityWrapper {
|
||||
return &CompatibilityWrapper{cache: cache}
|
||||
}
|
||||
|
||||
// CacheInterface implementation for backward compatibility
|
||||
func (c *CompatibilityWrapper) Set(key string, value interface{}, ttl time.Duration) {
|
||||
_ = c.cache.Set(key, value, ttl)
|
||||
}
|
||||
|
||||
func (c *CompatibilityWrapper) Get(key string) (interface{}, bool) {
|
||||
return c.cache.Get(key)
|
||||
}
|
||||
|
||||
func (c *CompatibilityWrapper) Delete(key string) {
|
||||
c.cache.Delete(key)
|
||||
}
|
||||
|
||||
func (c *CompatibilityWrapper) SetMaxSize(size int) {
|
||||
c.cache.SetMaxSize(size)
|
||||
}
|
||||
|
||||
func (c *CompatibilityWrapper) Size() int {
|
||||
return c.cache.Size()
|
||||
}
|
||||
|
||||
func (c *CompatibilityWrapper) Clear() {
|
||||
c.cache.Clear()
|
||||
}
|
||||
|
||||
func (c *CompatibilityWrapper) Cleanup() {
|
||||
c.cache.Cleanup()
|
||||
}
|
||||
|
||||
func (c *CompatibilityWrapper) Close() {
|
||||
_ = c.cache.Close()
|
||||
}
|
||||
|
||||
func (c *CompatibilityWrapper) GetStats() map[string]interface{} {
|
||||
return c.cache.GetStats()
|
||||
}
|
||||
|
||||
// UniversalCacheCompat provides compatibility with the old UniversalCache
|
||||
type UniversalCacheCompat struct {
|
||||
*Cache
|
||||
}
|
||||
|
||||
// NewUniversalCacheCompat creates a compatibility wrapper for UniversalCache
|
||||
func NewUniversalCacheCompat(config Config) *UniversalCacheCompat {
|
||||
return &UniversalCacheCompat{
|
||||
Cache: New(config),
|
||||
}
|
||||
}
|
||||
|
||||
// Set wraps the cache Set method for compatibility
|
||||
func (u *UniversalCacheCompat) Set(key string, value interface{}, ttl time.Duration) error {
|
||||
return u.Cache.Set(key, value, ttl)
|
||||
}
|
||||
|
||||
// TokenCacheCompat provides compatibility with the old TokenCache
|
||||
type TokenCacheCompat struct {
|
||||
cache *TokenCache
|
||||
}
|
||||
|
||||
// NewTokenCacheCompat creates a compatibility wrapper for TokenCache
|
||||
func NewTokenCacheCompat() *TokenCacheCompat {
|
||||
manager := GetGlobalManager(nil)
|
||||
return &TokenCacheCompat{
|
||||
cache: manager.GetTokenCache(),
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores parsed token claims
|
||||
func (t *TokenCacheCompat) Set(token string, claims map[string]interface{}, expiration time.Duration) {
|
||||
_ = t.cache.Set(token, claims, expiration)
|
||||
}
|
||||
|
||||
// Get retrieves cached claims for a token
|
||||
func (t *TokenCacheCompat) Get(token string) (map[string]interface{}, bool) {
|
||||
return t.cache.Get(token)
|
||||
}
|
||||
|
||||
// Delete removes a token from cache
|
||||
func (t *TokenCacheCompat) Delete(token string) {
|
||||
t.cache.Delete(token)
|
||||
}
|
||||
|
||||
// MetadataCacheCompat provides compatibility with the old MetadataCache
|
||||
type MetadataCacheCompat struct {
|
||||
cache *MetadataCache
|
||||
logger Logger
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewMetadataCacheCompat creates a compatibility wrapper for MetadataCache
|
||||
func NewMetadataCacheCompat(wg *sync.WaitGroup) *MetadataCacheCompat {
|
||||
manager := GetGlobalManager(nil)
|
||||
return &MetadataCacheCompat{
|
||||
cache: manager.GetMetadataCache(),
|
||||
logger: manager.logger,
|
||||
wg: wg,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMetadataCacheCompatWithLogger creates a MetadataCache with specific logger
|
||||
func NewMetadataCacheCompatWithLogger(wg *sync.WaitGroup, logger Logger) *MetadataCacheCompat {
|
||||
manager := GetGlobalManager(logger)
|
||||
return &MetadataCacheCompat{
|
||||
cache: manager.GetMetadataCache(),
|
||||
logger: logger,
|
||||
wg: wg,
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores provider metadata with a TTL
|
||||
func (m *MetadataCacheCompat) Set(providerURL string, metadata *ProviderMetadata, ttl time.Duration) error {
|
||||
return m.cache.Set(providerURL, metadata, ttl)
|
||||
}
|
||||
|
||||
// Get retrieves provider metadata from cache
|
||||
func (m *MetadataCacheCompat) Get(providerURL string) (*ProviderMetadata, bool) {
|
||||
return m.cache.Get(providerURL)
|
||||
}
|
||||
|
||||
// Delete removes provider metadata
|
||||
func (m *MetadataCacheCompat) Delete(providerURL string) {
|
||||
m.cache.Delete(providerURL)
|
||||
}
|
||||
|
||||
// GetWithGracePeriod retrieves metadata with grace period support
|
||||
func (m *MetadataCacheCompat) GetWithGracePeriod(ctx context.Context, providerURL string) (*ProviderMetadata, bool) {
|
||||
// For compatibility, just use regular Get
|
||||
return m.cache.Get(providerURL)
|
||||
}
|
||||
|
||||
// JWKCacheCompat provides compatibility with the old JWKCache
|
||||
type JWKCacheCompat struct {
|
||||
cache *JWKCache
|
||||
}
|
||||
|
||||
// NewJWKCacheCompat creates a compatibility wrapper for JWKCache
|
||||
func NewJWKCacheCompat() *JWKCacheCompat {
|
||||
manager := GetGlobalManager(nil)
|
||||
return &JWKCacheCompat{
|
||||
cache: manager.GetJWKCache(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetJWKS retrieves JWKS from cache or fetches from the remote URL if not cached
|
||||
func (j *JWKCacheCompat) GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error) {
|
||||
// Check cache first
|
||||
if jwks, found := j.cache.Get(jwksURL); found {
|
||||
return jwks, nil
|
||||
}
|
||||
|
||||
// For compatibility, we don't fetch from remote - that should be done by the caller
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Set stores a JWK set
|
||||
func (j *JWKCacheCompat) Set(jwksURL string, jwks *JWKSet, ttl time.Duration) error {
|
||||
return j.cache.Set(jwksURL, jwks, ttl)
|
||||
}
|
||||
|
||||
// Cleanup is a no-op for compatibility
|
||||
func (j *JWKCacheCompat) Cleanup() {}
|
||||
|
||||
// Close is a no-op for compatibility
|
||||
func (j *JWKCacheCompat) Close() {}
|
||||
|
||||
// CacheManagerCompat provides compatibility with the old CacheManager
|
||||
type CacheManagerCompat struct {
|
||||
manager *Manager
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// GetGlobalCacheManagerCompat returns a singleton CacheManager instance
|
||||
func GetGlobalCacheManagerCompat(wg *sync.WaitGroup) *CacheManagerCompat {
|
||||
return &CacheManagerCompat{
|
||||
manager: GetGlobalManager(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSharedTokenBlacklist returns the shared token blacklist cache
|
||||
func (c *CacheManagerCompat) GetSharedTokenBlacklist() *CompatibilityWrapper {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return NewCompatibilityWrapper(c.manager.GetRawTokenCache())
|
||||
}
|
||||
|
||||
// GetSharedTokenCache returns the shared token cache
|
||||
func (c *CacheManagerCompat) GetSharedTokenCache() *TokenCacheCompat {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return NewTokenCacheCompat()
|
||||
}
|
||||
|
||||
// GetSharedMetadataCache returns the shared metadata cache
|
||||
func (c *CacheManagerCompat) GetSharedMetadataCache() *MetadataCacheCompat {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return NewMetadataCacheCompat(nil)
|
||||
}
|
||||
|
||||
// GetSharedJWKCache returns the shared JWK cache
|
||||
func (c *CacheManagerCompat) GetSharedJWKCache() *JWKCacheCompat {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return NewJWKCacheCompat()
|
||||
}
|
||||
|
||||
// Close gracefully shuts down all cache components
|
||||
func (c *CacheManagerCompat) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.manager.Close()
|
||||
}
|
||||
|
||||
// UniversalCacheManagerCompat provides compatibility with UniversalCacheManager
|
||||
type UniversalCacheManagerCompat struct {
|
||||
manager *Manager
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// GetUniversalCacheManagerCompat returns the global cache manager
|
||||
func GetUniversalCacheManagerCompat(logger Logger) *UniversalCacheManagerCompat {
|
||||
return &UniversalCacheManagerCompat{
|
||||
manager: GetGlobalManager(logger),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTokenCache returns the token cache
|
||||
func (u *UniversalCacheManagerCompat) GetTokenCache() *UniversalCacheCompat {
|
||||
return &UniversalCacheCompat{
|
||||
Cache: u.manager.GetRawTokenCache(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetadataCache returns the metadata cache
|
||||
func (u *UniversalCacheManagerCompat) GetMetadataCache() *UniversalCacheCompat {
|
||||
return &UniversalCacheCompat{
|
||||
Cache: u.manager.GetRawMetadataCache(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetJWKCache returns the JWK cache
|
||||
func (u *UniversalCacheManagerCompat) GetJWKCache() *UniversalCacheCompat {
|
||||
return &UniversalCacheCompat{
|
||||
Cache: u.manager.GetRawJWKCache(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetBlacklistCache returns the blacklist cache (uses token cache)
|
||||
func (u *UniversalCacheManagerCompat) GetBlacklistCache() *UniversalCacheCompat {
|
||||
return &UniversalCacheCompat{
|
||||
Cache: u.manager.GetRawTokenCache(),
|
||||
}
|
||||
}
|
||||
|
||||
// Close shuts down the cache manager
|
||||
func (u *UniversalCacheManagerCompat) Close() error {
|
||||
return u.manager.Close()
|
||||
}
|
||||
Vendored
+284
@@ -0,0 +1,284 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Manager manages multiple cache instances with singleton pattern
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Core caches
|
||||
tokenCache *Cache
|
||||
metadataCache *Cache
|
||||
jwkCache *Cache
|
||||
sessionCache *Cache
|
||||
generalCache *Cache
|
||||
|
||||
// Typed wrappers
|
||||
typedToken *TokenCache
|
||||
typedMetadata *MetadataCache
|
||||
typedJWK *JWKCache
|
||||
typedSession *SessionCache
|
||||
|
||||
logger Logger
|
||||
}
|
||||
|
||||
var (
|
||||
globalManager *Manager
|
||||
globalManagerOnce sync.Once
|
||||
)
|
||||
|
||||
// GetGlobalManager returns the singleton cache manager instance
|
||||
func GetGlobalManager(logger Logger) *Manager {
|
||||
globalManagerOnce.Do(func() {
|
||||
globalManager = NewManager(logger)
|
||||
})
|
||||
return globalManager
|
||||
}
|
||||
|
||||
// NewManager creates a new cache manager
|
||||
func NewManager(logger Logger) *Manager {
|
||||
if logger == nil {
|
||||
logger = &noOpLogger{}
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Initialize core caches with appropriate configurations
|
||||
m.initializeCaches()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// initializeCaches creates all cache instances with appropriate configurations
|
||||
func (m *Manager) initializeCaches() {
|
||||
// Token cache configuration
|
||||
tokenConfig := Config{
|
||||
Type: TypeToken,
|
||||
MaxSize: 5000,
|
||||
MaxMemoryBytes: 32 * 1024 * 1024, // 32MB
|
||||
DefaultTTL: 1 * time.Hour,
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
EnableAutoCleanup: true,
|
||||
EnableMemoryLimit: true,
|
||||
EnableMetrics: true,
|
||||
Logger: m.logger,
|
||||
TokenConfig: &TokenConfig{
|
||||
BlacklistTTL: 24 * time.Hour,
|
||||
RefreshTokenTTL: 7 * 24 * time.Hour,
|
||||
EnableTokenRotation: true,
|
||||
},
|
||||
}
|
||||
m.tokenCache = New(tokenConfig)
|
||||
m.typedToken = NewTokenCache(m.tokenCache)
|
||||
|
||||
// Metadata cache configuration
|
||||
metadataConfig := Config{
|
||||
Type: TypeMetadata,
|
||||
MaxSize: 100,
|
||||
MaxMemoryBytes: 10 * 1024 * 1024, // 10MB
|
||||
DefaultTTL: 24 * time.Hour,
|
||||
CleanupInterval: 30 * time.Minute,
|
||||
EnableAutoCleanup: true,
|
||||
EnableMemoryLimit: true,
|
||||
EnableMetrics: true,
|
||||
Logger: m.logger,
|
||||
MetadataConfig: &MetadataConfig{
|
||||
GracePeriod: 5 * time.Minute,
|
||||
ExtendedGracePeriod: 15 * time.Minute,
|
||||
MaxGracePeriod: 1 * time.Hour,
|
||||
SecurityCriticalMaxGracePeriod: 30 * time.Minute,
|
||||
SecurityCriticalFields: []string{"issuer", "jwks_uri"},
|
||||
},
|
||||
}
|
||||
m.metadataCache = New(metadataConfig)
|
||||
m.typedMetadata = NewMetadataCache(m.metadataCache, *metadataConfig.MetadataConfig)
|
||||
|
||||
// JWK cache configuration
|
||||
jwkConfig := Config{
|
||||
Type: TypeJWK,
|
||||
MaxSize: 50,
|
||||
MaxMemoryBytes: 5 * 1024 * 1024, // 5MB
|
||||
DefaultTTL: 1 * time.Hour,
|
||||
CleanupInterval: 10 * time.Minute,
|
||||
EnableAutoCleanup: true,
|
||||
EnableMemoryLimit: true,
|
||||
EnableMetrics: true,
|
||||
Logger: m.logger,
|
||||
JWKConfig: &JWKConfig{
|
||||
RefreshInterval: 1 * time.Hour,
|
||||
MinRefreshTime: 5 * time.Minute,
|
||||
MaxKeyAge: 24 * time.Hour,
|
||||
},
|
||||
}
|
||||
m.jwkCache = New(jwkConfig)
|
||||
m.typedJWK = NewJWKCache(m.jwkCache)
|
||||
|
||||
// Session cache configuration
|
||||
sessionConfig := Config{
|
||||
Type: TypeSession,
|
||||
MaxSize: 10000,
|
||||
MaxMemoryBytes: 64 * 1024 * 1024, // 64MB
|
||||
DefaultTTL: 30 * time.Minute,
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
EnableAutoCleanup: true,
|
||||
EnableMemoryLimit: true,
|
||||
EnableMetrics: true,
|
||||
Logger: m.logger,
|
||||
}
|
||||
m.sessionCache = New(sessionConfig)
|
||||
m.typedSession = NewSessionCache(m.sessionCache)
|
||||
|
||||
// General cache configuration
|
||||
generalConfig := Config{
|
||||
Type: TypeGeneral,
|
||||
MaxSize: 1000,
|
||||
MaxMemoryBytes: 16 * 1024 * 1024, // 16MB
|
||||
DefaultTTL: 10 * time.Minute,
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
EnableAutoCleanup: true,
|
||||
EnableMemoryLimit: true,
|
||||
EnableMetrics: true,
|
||||
Logger: m.logger,
|
||||
}
|
||||
m.generalCache = New(generalConfig)
|
||||
}
|
||||
|
||||
// GetTokenCache returns the token cache instance
|
||||
func (m *Manager) GetTokenCache() *TokenCache {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.typedToken
|
||||
}
|
||||
|
||||
// GetMetadataCache returns the metadata cache instance
|
||||
func (m *Manager) GetMetadataCache() *MetadataCache {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.typedMetadata
|
||||
}
|
||||
|
||||
// GetJWKCache returns the JWK cache instance
|
||||
func (m *Manager) GetJWKCache() *JWKCache {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.typedJWK
|
||||
}
|
||||
|
||||
// GetSessionCache returns the session cache instance
|
||||
func (m *Manager) GetSessionCache() *SessionCache {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.typedSession
|
||||
}
|
||||
|
||||
// GetGeneralCache returns the general cache instance
|
||||
func (m *Manager) GetGeneralCache() *Cache {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.generalCache
|
||||
}
|
||||
|
||||
// GetRawTokenCache returns the raw token cache for compatibility
|
||||
func (m *Manager) GetRawTokenCache() *Cache {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.tokenCache
|
||||
}
|
||||
|
||||
// GetRawMetadataCache returns the raw metadata cache for compatibility
|
||||
func (m *Manager) GetRawMetadataCache() *Cache {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.metadataCache
|
||||
}
|
||||
|
||||
// GetRawJWKCache returns the raw JWK cache for compatibility
|
||||
func (m *Manager) GetRawJWKCache() *Cache {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.jwkCache
|
||||
}
|
||||
|
||||
// GetStats returns statistics for all caches
|
||||
func (m *Manager) GetStats() map[string]map[string]interface{} {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return map[string]map[string]interface{}{
|
||||
"token": m.tokenCache.GetStats(),
|
||||
"metadata": m.metadataCache.GetStats(),
|
||||
"jwk": m.jwkCache.GetStats(),
|
||||
"session": m.sessionCache.GetStats(),
|
||||
"general": m.generalCache.GetStats(),
|
||||
}
|
||||
}
|
||||
|
||||
// ClearAll clears all cache instances
|
||||
func (m *Manager) ClearAll() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.tokenCache.Clear()
|
||||
m.metadataCache.Clear()
|
||||
m.jwkCache.Clear()
|
||||
m.sessionCache.Clear()
|
||||
m.generalCache.Clear()
|
||||
}
|
||||
|
||||
// Close gracefully shuts down all cache instances
|
||||
func (m *Manager) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var firstErr error
|
||||
|
||||
if err := m.tokenCache.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := m.metadataCache.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := m.jwkCache.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := m.sessionCache.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := m.generalCache.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// CleanupAll runs cleanup on all cache instances
|
||||
func (m *Manager) CleanupAll() {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
m.tokenCache.Cleanup()
|
||||
m.metadataCache.Cleanup()
|
||||
m.jwkCache.Cleanup()
|
||||
m.sessionCache.Cleanup()
|
||||
m.generalCache.Cleanup()
|
||||
}
|
||||
|
||||
// SetLogger updates the logger for all caches
|
||||
func (m *Manager) SetLogger(logger Logger) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.logger = logger
|
||||
if logger != nil {
|
||||
m.tokenCache.logger = logger
|
||||
m.metadataCache.logger = logger
|
||||
m.jwkCache.logger = logger
|
||||
m.sessionCache.logger = logger
|
||||
m.generalCache.logger = logger
|
||||
}
|
||||
}
|
||||
Vendored
+315
@@ -0,0 +1,315 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TypedCache provides a type-safe wrapper around Cache for specific types
|
||||
type TypedCache[T any] struct {
|
||||
cache *Cache
|
||||
prefix string
|
||||
}
|
||||
|
||||
// NewTypedCache creates a new typed cache wrapper
|
||||
func NewTypedCache[T any](cache *Cache, prefix string) *TypedCache[T] {
|
||||
return &TypedCache[T]{
|
||||
cache: cache,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores a typed value
|
||||
func (tc *TypedCache[T]) Set(key string, value T, ttl time.Duration) error {
|
||||
prefixedKey := tc.prefix + key
|
||||
return tc.cache.Set(prefixedKey, value, ttl)
|
||||
}
|
||||
|
||||
// Get retrieves a typed value
|
||||
func (tc *TypedCache[T]) Get(key string) (T, bool) {
|
||||
var zero T
|
||||
prefixedKey := tc.prefix + key
|
||||
|
||||
value, exists := tc.cache.Get(prefixedKey)
|
||||
if !exists {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// Try direct type assertion first
|
||||
if typedValue, ok := value.(T); ok {
|
||||
return typedValue, true
|
||||
}
|
||||
|
||||
// If that fails, try JSON marshaling/unmarshaling for complex types
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
var result T
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return result, true
|
||||
}
|
||||
|
||||
// Delete removes a typed value
|
||||
func (tc *TypedCache[T]) Delete(key string) {
|
||||
prefixedKey := tc.prefix + key
|
||||
tc.cache.Delete(prefixedKey)
|
||||
}
|
||||
|
||||
// Clear removes all items with the prefix
|
||||
func (tc *TypedCache[T]) Clear() {
|
||||
// Note: This clears the entire underlying cache
|
||||
// In a production system, you might want to implement prefix-based clearing
|
||||
tc.cache.Clear()
|
||||
}
|
||||
|
||||
// Size returns the size of the underlying cache
|
||||
func (tc *TypedCache[T]) Size() int {
|
||||
return tc.cache.Size()
|
||||
}
|
||||
|
||||
// TokenCache provides specialized caching for JWT tokens
|
||||
type TokenCache struct {
|
||||
cache *TypedCache[map[string]interface{}]
|
||||
}
|
||||
|
||||
// NewTokenCache creates a new token cache
|
||||
func NewTokenCache(baseCache *Cache) *TokenCache {
|
||||
return &TokenCache{
|
||||
cache: NewTypedCache[map[string]interface{}](baseCache, "token:"),
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores parsed token claims
|
||||
func (tc *TokenCache) Set(token string, claims map[string]interface{}, expiration time.Duration) error {
|
||||
return tc.cache.Set(token, claims, expiration)
|
||||
}
|
||||
|
||||
// Get retrieves cached claims for a token
|
||||
func (tc *TokenCache) Get(token string) (map[string]interface{}, bool) {
|
||||
return tc.cache.Get(token)
|
||||
}
|
||||
|
||||
// Delete removes a token from cache
|
||||
func (tc *TokenCache) Delete(token string) {
|
||||
tc.cache.Delete(token)
|
||||
}
|
||||
|
||||
// SetBlacklisted marks a token as blacklisted
|
||||
func (tc *TokenCache) SetBlacklisted(token string, ttl time.Duration) error {
|
||||
blacklistKey := "blacklist:" + token
|
||||
// Store blacklisted status as a map to match the type
|
||||
blacklistData := map[string]interface{}{"blacklisted": true}
|
||||
return tc.cache.Set(blacklistKey, blacklistData, ttl)
|
||||
}
|
||||
|
||||
// IsBlacklisted checks if a token is blacklisted
|
||||
func (tc *TokenCache) IsBlacklisted(token string) bool {
|
||||
blacklistKey := "blacklist:" + token
|
||||
value, exists := tc.cache.Get(blacklistKey)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
// Check if the blacklist data indicates blacklisted status
|
||||
if data, ok := value["blacklisted"]; ok {
|
||||
blacklisted, _ := data.(bool)
|
||||
return blacklisted
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MetadataCache provides specialized caching for provider metadata
|
||||
type MetadataCache struct {
|
||||
cache *Cache
|
||||
config MetadataConfig
|
||||
}
|
||||
|
||||
// ProviderMetadata represents OIDC provider metadata
|
||||
type ProviderMetadata struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
UserInfoEndpoint string `json:"userinfo_endpoint"`
|
||||
JWKSUri string `json:"jwks_uri"`
|
||||
ScopesSupported []string `json:"scopes_supported"`
|
||||
}
|
||||
|
||||
// NewMetadataCache creates a new metadata cache
|
||||
func NewMetadataCache(baseCache *Cache, config MetadataConfig) *MetadataCache {
|
||||
return &MetadataCache{
|
||||
cache: baseCache,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores provider metadata with grace period support
|
||||
func (mc *MetadataCache) Set(providerURL string, metadata *ProviderMetadata, ttl time.Duration) error {
|
||||
if metadata == nil {
|
||||
return fmt.Errorf("metadata cannot be nil")
|
||||
}
|
||||
|
||||
key := "metadata:" + providerURL
|
||||
|
||||
// Apply grace period if configured
|
||||
if mc.config.GracePeriod > 0 {
|
||||
ttl += mc.config.GracePeriod
|
||||
}
|
||||
|
||||
// Store as JSON for consistency
|
||||
data, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
return mc.cache.Set(key, data, ttl)
|
||||
}
|
||||
|
||||
// Get retrieves provider metadata from cache
|
||||
func (mc *MetadataCache) Get(providerURL string) (*ProviderMetadata, bool) {
|
||||
key := "metadata:" + providerURL
|
||||
value, exists := mc.cache.Get(key)
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Handle different value types
|
||||
var data []byte
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
data = v
|
||||
case string:
|
||||
data = []byte(v)
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var metadata ProviderMetadata
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &metadata, true
|
||||
}
|
||||
|
||||
// Delete removes provider metadata
|
||||
func (mc *MetadataCache) Delete(providerURL string) {
|
||||
key := "metadata:" + providerURL
|
||||
mc.cache.Delete(key)
|
||||
}
|
||||
|
||||
// JWKCache provides specialized caching for JWK sets
|
||||
type JWKCache struct {
|
||||
cache *Cache
|
||||
}
|
||||
|
||||
// JWKSet represents a set of JSON Web Keys
|
||||
type JWKSet struct {
|
||||
Keys []JWK `json:"keys"`
|
||||
}
|
||||
|
||||
// JWK represents a JSON Web Key
|
||||
type JWK struct {
|
||||
Kid string `json:"kid"`
|
||||
Kty string `json:"kty"`
|
||||
Use string `json:"use"`
|
||||
N string `json:"n"`
|
||||
E string `json:"e"`
|
||||
X5c []string `json:"x5c,omitempty"`
|
||||
}
|
||||
|
||||
// NewJWKCache creates a new JWK cache
|
||||
func NewJWKCache(baseCache *Cache) *JWKCache {
|
||||
return &JWKCache{
|
||||
cache: baseCache,
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores a JWK set
|
||||
func (jc *JWKCache) Set(jwksURL string, jwks *JWKSet, ttl time.Duration) error {
|
||||
if jwks == nil {
|
||||
return fmt.Errorf("JWK set cannot be nil")
|
||||
}
|
||||
|
||||
key := "jwk:" + jwksURL
|
||||
return jc.cache.Set(key, jwks, ttl)
|
||||
}
|
||||
|
||||
// Get retrieves a JWK set from cache
|
||||
func (jc *JWKCache) Get(jwksURL string) (*JWKSet, bool) {
|
||||
key := "jwk:" + jwksURL
|
||||
value, exists := jc.cache.Get(key)
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
jwks, ok := value.(*JWKSet)
|
||||
if !ok {
|
||||
// Try JSON conversion
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var result JWKSet
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &result, true
|
||||
}
|
||||
|
||||
return jwks, true
|
||||
}
|
||||
|
||||
// Delete removes a JWK set from cache
|
||||
func (jc *JWKCache) Delete(jwksURL string) {
|
||||
key := "jwk:" + jwksURL
|
||||
jc.cache.Delete(key)
|
||||
}
|
||||
|
||||
// SessionCache provides specialized caching for sessions
|
||||
type SessionCache struct {
|
||||
cache *TypedCache[SessionData]
|
||||
}
|
||||
|
||||
// SessionData represents session information
|
||||
type SessionData struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Claims map[string]interface{} `json:"claims"`
|
||||
}
|
||||
|
||||
// NewSessionCache creates a new session cache
|
||||
func NewSessionCache(baseCache *Cache) *SessionCache {
|
||||
return &SessionCache{
|
||||
cache: NewTypedCache[SessionData](baseCache, "session:"),
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores session data
|
||||
func (sc *SessionCache) Set(sessionID string, data SessionData, ttl time.Duration) error {
|
||||
return sc.cache.Set(sessionID, data, ttl)
|
||||
}
|
||||
|
||||
// Get retrieves session data
|
||||
func (sc *SessionCache) Get(sessionID string) (SessionData, bool) {
|
||||
return sc.cache.Get(sessionID)
|
||||
}
|
||||
|
||||
// Delete removes a session
|
||||
func (sc *SessionCache) Delete(sessionID string) {
|
||||
sc.cache.Delete(sessionID)
|
||||
}
|
||||
|
||||
// Exists checks if a session exists
|
||||
func (sc *SessionCache) Exists(sessionID string) bool {
|
||||
_, exists := sc.cache.Get(sessionID)
|
||||
return exists
|
||||
}
|
||||
Reference in New Issue
Block a user