mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-13 04:07:37 +00:00
fixup! fixup! Further pursue of perfection.
This commit is contained in:
@@ -144,9 +144,17 @@ func (c *Cache) Delete(key string) {
|
||||
// This method is called automatically by the background cleanup task but can also
|
||||
// be called manually for immediate cleanup of expired entries.
|
||||
func (c *Cache) Cleanup() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if c.items == nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for key, item := range c.items {
|
||||
if now.After(item.ExpiresAt) {
|
||||
|
||||
+13
-3
@@ -172,20 +172,30 @@ func (c *OptimizedCache) Delete(key string) {
|
||||
// It scans the LRU list for expired entries and enforces memory limits
|
||||
// by evicting least recently used items if necessary.
|
||||
func (c *OptimizedCache) Cleanup() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if c.items == nil || c.head == nil || c.tail == nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
toRemove := make([]*OptimizedCacheEntry, 0, len(c.items)/10)
|
||||
|
||||
for entry := c.head.next; entry != c.tail; entry = entry.next {
|
||||
if now.After(entry.ExpiresAt) {
|
||||
for entry := c.head.next; entry != nil && entry != c.tail; entry = entry.next {
|
||||
if entry != nil && now.After(entry.ExpiresAt) {
|
||||
toRemove = append(toRemove, entry)
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range toRemove {
|
||||
c.removeEntry(entry)
|
||||
if entry != nil {
|
||||
c.removeEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
for c.currentMemoryBytes > c.maxMemoryBytes && len(c.items) > 0 {
|
||||
|
||||
@@ -364,33 +364,24 @@ func TestReplayCacheBasic(t *testing.T) {
|
||||
// Test audience verification
|
||||
func TestAudienceVerify(t *testing.T) {
|
||||
// Exact match
|
||||
claims1 := map[string]interface{}{
|
||||
"aud": "my-client",
|
||||
}
|
||||
if err := verifyAudience(claims1, "my-client"); err != nil {
|
||||
if err := verifyAudience("my-client", "my-client"); err != nil {
|
||||
t.Errorf("Exact match should succeed: %v", err)
|
||||
}
|
||||
|
||||
// Array contains
|
||||
claims2 := map[string]interface{}{
|
||||
"aud": []interface{}{"client1", "my-client", "client2"},
|
||||
}
|
||||
if err := verifyAudience(claims2, "my-client"); err != nil {
|
||||
audArray := []interface{}{"client1", "my-client", "client2"}
|
||||
if err := verifyAudience(audArray, "my-client"); err != nil {
|
||||
t.Errorf("Array contains should succeed: %v", err)
|
||||
}
|
||||
|
||||
// Mismatch
|
||||
claims3 := map[string]interface{}{
|
||||
"aud": "other-client",
|
||||
}
|
||||
if err := verifyAudience(claims3, "my-client"); err == nil {
|
||||
if err := verifyAudience("other-client", "my-client"); err == nil {
|
||||
t.Error("Mismatch should fail")
|
||||
}
|
||||
|
||||
// Missing
|
||||
claims4 := map[string]interface{}{}
|
||||
if err := verifyAudience(claims4, "my-client"); err == nil {
|
||||
t.Error("Missing aud should fail")
|
||||
// Missing/nil audience
|
||||
if err := verifyAudience(nil, "my-client"); err == nil {
|
||||
t.Error("Nil aud should fail")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ func TestInputValidatorExtended(t *testing.T) {
|
||||
func TestSessionManagerCreation(t *testing.T) {
|
||||
logger := NewLogger("debug")
|
||||
|
||||
sm, err := NewSessionManager("test-secret", false, "Lax", logger)
|
||||
// Use a 32-byte encryption key as required
|
||||
encryptionKey := "12345678901234567890123456789012"
|
||||
sm, err := NewSessionManager(encryptionKey, false, "Lax", logger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSessionManager failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -589,6 +589,128 @@ func (e *HTTPError) Error() string {
|
||||
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
// OIDCError represents OIDC-specific errors with context information.
|
||||
// It provides structured error reporting for authentication and authorization failures.
|
||||
type OIDCError struct {
|
||||
// Code identifies the specific error type
|
||||
Code string
|
||||
// Message provides a human-readable description
|
||||
Message string
|
||||
// Context contains additional error context (e.g., provider, session details)
|
||||
Context map[string]interface{}
|
||||
// Cause is the underlying error that caused this error
|
||||
Cause error
|
||||
}
|
||||
|
||||
// Error returns the string representation of the OIDC error.
|
||||
// Implements the error interface.
|
||||
func (e *OIDCError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("OIDC error [%s]: %s - caused by: %v", e.Code, e.Message, e.Cause)
|
||||
}
|
||||
return fmt.Sprintf("OIDC error [%s]: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error for error chain unwrapping.
|
||||
func (e *OIDCError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// SessionError represents session-related errors with context.
|
||||
// Used for session management, validation, and storage errors.
|
||||
type SessionError struct {
|
||||
// Operation describes what session operation failed
|
||||
Operation string
|
||||
// Message provides a human-readable description
|
||||
Message string
|
||||
// SessionID identifies the session (if available)
|
||||
SessionID string
|
||||
// Cause is the underlying error that caused this error
|
||||
Cause error
|
||||
}
|
||||
|
||||
// Error returns the string representation of the session error.
|
||||
// Implements the error interface.
|
||||
func (e *SessionError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("Session error in %s: %s - caused by: %v", e.Operation, e.Message, e.Cause)
|
||||
}
|
||||
return fmt.Sprintf("Session error in %s: %s", e.Operation, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error for error chain unwrapping.
|
||||
func (e *SessionError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// TokenError represents token-related errors with validation context.
|
||||
// Used for JWT validation, token refresh, and token format errors.
|
||||
type TokenError struct {
|
||||
// TokenType identifies the type of token (id_token, access_token, refresh_token)
|
||||
TokenType string
|
||||
// Reason describes why the token is invalid
|
||||
Reason string
|
||||
// Message provides a human-readable description
|
||||
Message string
|
||||
// Cause is the underlying error that caused this error
|
||||
Cause error
|
||||
}
|
||||
|
||||
// Error returns the string representation of the token error.
|
||||
// Implements the error interface.
|
||||
func (e *TokenError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("Token error (%s) - %s: %s - caused by: %v", e.TokenType, e.Reason, e.Message, e.Cause)
|
||||
}
|
||||
return fmt.Sprintf("Token error (%s) - %s: %s", e.TokenType, e.Reason, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error for error chain unwrapping.
|
||||
func (e *TokenError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// NewOIDCError creates a new OIDC error with context.
|
||||
func NewOIDCError(code, message string, cause error) *OIDCError {
|
||||
return &OIDCError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Context: make(map[string]interface{}),
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext adds context information to the OIDC error.
|
||||
func (e *OIDCError) WithContext(key string, value interface{}) *OIDCError {
|
||||
e.Context[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
// NewSessionError creates a new session error with operation context.
|
||||
func NewSessionError(operation, message string, cause error) *SessionError {
|
||||
return &SessionError{
|
||||
Operation: operation,
|
||||
Message: message,
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// WithSessionID adds session ID to the session error.
|
||||
func (e *SessionError) WithSessionID(sessionID string) *SessionError {
|
||||
e.SessionID = sessionID
|
||||
return e
|
||||
}
|
||||
|
||||
// NewTokenError creates a new token error with type and reason.
|
||||
func NewTokenError(tokenType, reason, message string, cause error) *TokenError {
|
||||
return &TokenError{
|
||||
TokenType: tokenType,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// GracefulDegradation implements graceful degradation patterns for service resilience.
|
||||
// It provides fallback mechanisms when primary services are unavailable and monitors
|
||||
// service health to automatically recover when services become available again.
|
||||
|
||||
+3
-1
@@ -263,7 +263,9 @@ func (tc *TokenCache) Delete(token string) {
|
||||
// This is typically called automatically but can be invoked manually for
|
||||
// removing expired token entries.
|
||||
func (tc *TokenCache) Cleanup() {
|
||||
tc.cache.Cleanup()
|
||||
if tc != nil && tc.cache != nil {
|
||||
tc.cache.Cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the cleanup goroutine and releases resources.
|
||||
|
||||
@@ -40,7 +40,7 @@ type HTTPClientConfig struct {
|
||||
// DefaultHTTPClientConfig returns the default configuration for general use
|
||||
func DefaultHTTPClientConfig() HTTPClientConfig {
|
||||
return HTTPClientConfig{
|
||||
Timeout: 5 * time.Second,
|
||||
Timeout: 30 * time.Second,
|
||||
MaxRedirects: 10,
|
||||
UseCookieJar: false,
|
||||
DialTimeout: 5 * time.Second,
|
||||
@@ -63,8 +63,9 @@ func DefaultHTTPClientConfig() HTTPClientConfig {
|
||||
// TokenHTTPClientConfig returns configuration optimized for token operations
|
||||
func TokenHTTPClientConfig() HTTPClientConfig {
|
||||
config := DefaultHTTPClientConfig()
|
||||
config.MaxRedirects = 50 // Token endpoints may redirect more
|
||||
config.UseCookieJar = true // Enable cookie jar for token operations
|
||||
config.Timeout = 10 * time.Second // Shorter timeout for token operations
|
||||
config.MaxRedirects = 50 // Token endpoints may redirect more
|
||||
config.UseCookieJar = true // Enable cookie jar for token operations
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ func (c *JWKCache) GetJWKS(ctx context.Context, jwksURL string, httpClient *http
|
||||
// Cleanup removes expired entries from the JWK cache to free memory.
|
||||
// It delegates to the internal cache's cleanup method.
|
||||
func (c *JWKCache) Cleanup() {
|
||||
if c.internalCache != nil {
|
||||
if c != nil && c.internalCache != nil {
|
||||
c.internalCache.Cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func getReplayCacheStats() (size int, maxSize int) {
|
||||
defer replayCacheMu.RUnlock()
|
||||
|
||||
if replayCache == nil {
|
||||
return 0, 0
|
||||
return 0, 10000
|
||||
}
|
||||
|
||||
return 0, 10000
|
||||
|
||||
@@ -751,16 +751,7 @@ func (t *TraefikOidc) updateMetadataEndpoints(metadata *ProviderMetadata) {
|
||||
func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
||||
ticker := time.NewTicker(2 * time.Hour)
|
||||
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Add(1)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Done()
|
||||
}
|
||||
}()
|
||||
defer ticker.Stop()
|
||||
|
||||
consecutiveFailures := 0
|
||||
@@ -925,7 +916,17 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if !t.metadataRefreshStarted && t.providerURL != "" {
|
||||
t.metadataRefreshStarted = true
|
||||
go t.startMetadataRefresh(t.providerURL)
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Add(1)
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Done()
|
||||
}
|
||||
}()
|
||||
t.startMetadataRefresh(t.providerURL)
|
||||
}()
|
||||
}
|
||||
}
|
||||
t.firstRequestMutex.Unlock()
|
||||
@@ -1801,44 +1802,80 @@ func (t *TraefikOidc) validateHost(host string) error {
|
||||
// It runs periodic cleanup of token cache, JWK cache, and session chunks.
|
||||
// Includes panic recovery to ensure stability.
|
||||
func (t *TraefikOidc) startTokenCleanup() {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Capture values to avoid race conditions
|
||||
tokenCache := t.tokenCache
|
||||
jwkCache := t.jwkCache
|
||||
sessionManager := t.sessionManager
|
||||
logger := t.logger
|
||||
goroutineWG := t.goroutineWG
|
||||
stopChan := t.tokenCleanupStopChan
|
||||
ctx := t.ctx
|
||||
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Add(1)
|
||||
if goroutineWG != nil {
|
||||
goroutineWG.Add(1)
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if t.goroutineWG != nil {
|
||||
t.goroutineWG.Done()
|
||||
if goroutineWG != nil {
|
||||
goroutineWG.Done()
|
||||
}
|
||||
ticker.Stop()
|
||||
|
||||
if r := recover(); r != nil {
|
||||
t.logger.Errorf("Token cleanup goroutine panic recovered: %v", r)
|
||||
if logger != nil {
|
||||
logger.Errorf("Token cleanup goroutine panic recovered: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
t.logger.Debug("Starting token cleanup cycle")
|
||||
if t.tokenCache != nil {
|
||||
t.tokenCache.Cleanup()
|
||||
if logger != nil {
|
||||
logger.Debug("Starting token cleanup cycle")
|
||||
}
|
||||
if t.jwkCache != nil {
|
||||
t.jwkCache.Cleanup()
|
||||
if tokenCache != nil {
|
||||
tokenCache.Cleanup()
|
||||
}
|
||||
if t.sessionManager != nil {
|
||||
t.sessionManager.PeriodicChunkCleanup()
|
||||
|
||||
t.logger.Debug("Running session health monitoring")
|
||||
if jwkCache != nil {
|
||||
jwkCache.Cleanup()
|
||||
}
|
||||
if sessionManager != nil {
|
||||
sessionManager.PeriodicChunkCleanup()
|
||||
if logger != nil {
|
||||
logger.Debug("Running session health monitoring")
|
||||
}
|
||||
}
|
||||
|
||||
case <-t.tokenCleanupStopChan:
|
||||
t.logger.Debug("Token cleanup goroutine stopped.")
|
||||
case <-func() <-chan struct{} {
|
||||
if stopChan != nil {
|
||||
return stopChan
|
||||
}
|
||||
// Return a channel that never receives anything
|
||||
c := make(chan struct{})
|
||||
return c
|
||||
}():
|
||||
if logger != nil {
|
||||
logger.Debug("Token cleanup goroutine stopped.")
|
||||
}
|
||||
return
|
||||
case <-t.ctx.Done():
|
||||
t.logger.Debug("Token cleanup goroutine stopped due to context cancellation.")
|
||||
case <-func() <-chan struct{} {
|
||||
if ctx != nil {
|
||||
return ctx.Done()
|
||||
}
|
||||
// Return a channel that never receives anything
|
||||
c := make(chan struct{})
|
||||
return c
|
||||
}():
|
||||
if logger != nil {
|
||||
logger.Debug("Token cleanup goroutine stopped due to context cancellation.")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -275,21 +275,27 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain strin
|
||||
// It cleans up orphaned token chunks, expired sessions, and unused pool objects.
|
||||
// This helps maintain performance and prevent cookie accumulation in client browsers.
|
||||
func (sm *SessionManager) PeriodicChunkCleanup() {
|
||||
if sm == nil || sm.logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sm.logger.Debug("Starting comprehensive session cleanup cycle")
|
||||
|
||||
cleanupStart := time.Now()
|
||||
var orphanedChunks, expiredSessions, cleanupErrors int
|
||||
|
||||
if cookieStore, ok := sm.store.(*sessions.CookieStore); ok {
|
||||
sm.logger.Debug("Running session store cleanup")
|
||||
_ = cookieStore
|
||||
if sm.store != nil {
|
||||
if cookieStore, ok := sm.store.(*sessions.CookieStore); ok {
|
||||
sm.logger.Debug("Running session store cleanup")
|
||||
_ = cookieStore
|
||||
}
|
||||
}
|
||||
|
||||
poolCleaned := 0
|
||||
for i := 0; i < 10; i++ {
|
||||
if poolSession := sm.sessionPool.Get(); poolSession != nil {
|
||||
sessionData := poolSession.(*SessionData)
|
||||
if sessionData != nil && !sessionData.inUse {
|
||||
sessionData, ok := poolSession.(*SessionData)
|
||||
if ok && sessionData != nil && !sessionData.inUse {
|
||||
sessionData.Reset()
|
||||
poolCleaned++
|
||||
}
|
||||
@@ -861,10 +867,6 @@ func (sd *SessionData) clearAllSessionData(r *http.Request, expire bool) {
|
||||
// - An error if session saving fails during cleanup.
|
||||
func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
sd.returnToPoolSafely()
|
||||
panic(rec)
|
||||
}
|
||||
sd.returnToPoolSafely()
|
||||
}()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user