mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-20 04:57:02 +00:00
Smarter approach to the cookies (#103)
* Smarter approach to the cookies - Single maxCookieSize = 1400 constant with clear documentation - Combined cookie storage for ~40-45% size reduction - Backward compatible migration from legacy cookies * Tuneup the code.
This commit is contained in:
+4
-4
@@ -84,8 +84,8 @@ func TestAudienceValidation(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
audience string
|
audience string
|
||||||
expectError bool
|
|
||||||
errorContains string
|
errorContains string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid custom audience URL",
|
name: "valid custom audience URL",
|
||||||
@@ -163,8 +163,8 @@ func TestConfigAudienceValidation(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
audience string
|
audience string
|
||||||
wantErr bool
|
|
||||||
errContains string
|
errContains string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Empty audience is valid for backward compatibility",
|
name: "Empty audience is valid for backward compatibility",
|
||||||
@@ -732,11 +732,11 @@ func TestJWTAudienceVerification(t *testing.T) {
|
|||||||
tokenCache := tc.addTokenCache(NewTokenCache())
|
tokenCache := tc.addTokenCache(NewTokenCache())
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
tokenAudience interface{}
|
||||||
name string
|
name string
|
||||||
configAudience string
|
configAudience string
|
||||||
tokenAudience interface{}
|
|
||||||
wantErr bool
|
|
||||||
errContains string
|
errContains string
|
||||||
|
wantErr bool
|
||||||
skipReplayCheck bool
|
skipReplayCheck bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -253,8 +253,8 @@ func (s *AuthFlowBehaviourSuite) TestPrepareSessionForAuthentication_WithPKCE()
|
|||||||
// TestIsAjaxRequest tests AJAX request detection
|
// TestIsAjaxRequest tests AJAX request detection
|
||||||
func (s *AuthFlowBehaviourSuite) TestIsAjaxRequest() {
|
func (s *AuthFlowBehaviourSuite) TestIsAjaxRequest() {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
|
||||||
headers map[string]string
|
headers map[string]string
|
||||||
|
name string
|
||||||
expectAjax bool
|
expectAjax bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
|||||||
+10
-11
@@ -222,17 +222,16 @@ func (bt *BackgroundTask) run() {
|
|||||||
// TaskCircuitBreaker implements circuit breaker pattern for background task creation
|
// TaskCircuitBreaker implements circuit breaker pattern for background task creation
|
||||||
// It limits concurrent task execution and tracks failures to prevent system overload
|
// It limits concurrent task execution and tracks failures to prevent system overload
|
||||||
type TaskCircuitBreaker struct {
|
type TaskCircuitBreaker struct {
|
||||||
state int32 // CircuitBreakerState
|
|
||||||
failureCount int32
|
|
||||||
lastFailureTime int64 // Unix timestamp
|
|
||||||
failureThreshold int32
|
|
||||||
timeout time.Duration
|
|
||||||
logger *Logger
|
logger *Logger
|
||||||
// Concurrency limiting
|
activeTasks map[string]struct{}
|
||||||
concurrentTasks int32 // Current number of running tasks
|
lastFailureTime int64
|
||||||
maxConcurrent int32 // Maximum concurrent tasks allowed
|
timeout time.Duration
|
||||||
activeTasks map[string]struct{} // Track active task names
|
tasksMu sync.RWMutex
|
||||||
tasksMu sync.RWMutex // Separate mutex for task tracking
|
state int32
|
||||||
|
failureCount int32
|
||||||
|
failureThreshold int32
|
||||||
|
concurrentTasks int32
|
||||||
|
maxConcurrent int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTaskCircuitBreaker creates a new circuit breaker for background tasks
|
// NewTaskCircuitBreaker creates a new circuit breaker for background tasks
|
||||||
@@ -380,9 +379,9 @@ func (cb *TaskCircuitBreaker) OnTaskFailure(taskName string, err error) {
|
|||||||
// TaskRegistry maintains a registry of all active background tasks to prevent duplicates
|
// TaskRegistry maintains a registry of all active background tasks to prevent duplicates
|
||||||
type TaskRegistry struct {
|
type TaskRegistry struct {
|
||||||
tasks map[string]*BackgroundTask
|
tasks map[string]*BackgroundTask
|
||||||
mu sync.RWMutex
|
|
||||||
cb *TaskCircuitBreaker
|
cb *TaskCircuitBreaker
|
||||||
logger *Logger
|
logger *Logger
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// GlobalTaskRegistry is the singleton instance for managing all background tasks
|
// GlobalTaskRegistry is the singleton instance for managing all background tasks
|
||||||
|
|||||||
+6
-6
@@ -330,12 +330,12 @@ func TestValidateGoogleTokens(t *testing.T) {
|
|||||||
ts.tOidc.refreshGracePeriod = 60 * time.Second
|
ts.tOidc.refreshGracePeriod = 60 * time.Second
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
setupSession func() *SessionData
|
setupSession func() *SessionData
|
||||||
|
name string
|
||||||
|
description string
|
||||||
expectedAuth bool
|
expectedAuth bool
|
||||||
expectedRefresh bool
|
expectedRefresh bool
|
||||||
expectedExpired bool
|
expectedExpired bool
|
||||||
description string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "ValidGoogleTokens",
|
name: "ValidGoogleTokens",
|
||||||
@@ -476,13 +476,13 @@ func TestIsUserAuthenticated(t *testing.T) {
|
|||||||
ts.tOidc.refreshGracePeriod = 60 * time.Second
|
ts.tOidc.refreshGracePeriod = 60 * time.Second
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
setupSession func() *SessionData
|
||||||
name string
|
name string
|
||||||
providerType string
|
providerType string
|
||||||
setupSession func() *SessionData
|
description string
|
||||||
expectedAuth bool
|
expectedAuth bool
|
||||||
expectedRefresh bool
|
expectedRefresh bool
|
||||||
expectedExpired bool
|
expectedExpired bool
|
||||||
description string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "AzureProvider",
|
name: "AzureProvider",
|
||||||
@@ -660,12 +660,12 @@ func TestValidateAzureTokensEdgeCases(t *testing.T) {
|
|||||||
ts.tOidc.refreshGracePeriod = 60 * time.Second
|
ts.tOidc.refreshGracePeriod = 60 * time.Second
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
setupSession func() *SessionData
|
setupSession func() *SessionData
|
||||||
|
name string
|
||||||
|
description string
|
||||||
expectedAuth bool
|
expectedAuth bool
|
||||||
expectedRefresh bool
|
expectedRefresh bool
|
||||||
expectedExpired bool
|
expectedExpired bool
|
||||||
description string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "UnauthenticatedWithRefreshToken",
|
name: "UnauthenticatedWithRefreshToken",
|
||||||
|
|||||||
@@ -97,15 +97,15 @@ func TestMemoryMonitorComprehensive(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("String method returns pressure name", func(t *testing.T) {
|
t.Run("String method returns pressure name", func(t *testing.T) {
|
||||||
pressures := []struct {
|
pressures := []struct {
|
||||||
level MemoryPressureLevel
|
|
||||||
name string
|
name string
|
||||||
|
level MemoryPressureLevel
|
||||||
}{
|
}{
|
||||||
{MemoryPressureNone, "None"},
|
{level: MemoryPressureNone, name: "None"},
|
||||||
{MemoryPressureLow, "Low"},
|
{level: MemoryPressureLow, name: "Low"},
|
||||||
{MemoryPressureModerate, "Moderate"},
|
{level: MemoryPressureModerate, name: "Moderate"},
|
||||||
{MemoryPressureHigh, "High"},
|
{level: MemoryPressureHigh, name: "High"},
|
||||||
{MemoryPressureCritical, "Critical"},
|
{level: MemoryPressureCritical, name: "Critical"},
|
||||||
{MemoryPressureLevel(999), "Unknown"},
|
{level: MemoryPressureLevel(999), name: "Unknown"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range pressures {
|
for _, p := range pressures {
|
||||||
|
|||||||
+3
-3
@@ -155,9 +155,9 @@ type CacheStrategy interface {
|
|||||||
|
|
||||||
// CacheEntry for backward compatibility
|
// CacheEntry for backward compatibility
|
||||||
type CacheEntry struct {
|
type CacheEntry struct {
|
||||||
Key string
|
|
||||||
Value interface{}
|
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
|
Value interface{}
|
||||||
|
Key string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache is an alias for backward compatibility
|
// Cache is an alias for backward compatibility
|
||||||
@@ -175,10 +175,10 @@ func NewOptimizedCacheWithConfig(config OptimizedCacheConfig) *CacheInterfaceWra
|
|||||||
|
|
||||||
// ListNode for backward compatibility
|
// ListNode for backward compatibility
|
||||||
type ListNode struct {
|
type ListNode struct {
|
||||||
Key string
|
|
||||||
Value interface{}
|
Value interface{}
|
||||||
Next *ListNode
|
Next *ListNode
|
||||||
Prev *ListNode
|
Prev *ListNode
|
||||||
|
Key string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFixedMetadataCache creates a metadata cache with fixed configuration
|
// NewFixedMetadataCache creates a metadata cache with fixed configuration
|
||||||
|
|||||||
+11
-11
@@ -19,16 +19,16 @@ import (
|
|||||||
|
|
||||||
// CacheTestCase represents a comprehensive test case for cache operations
|
// CacheTestCase represents a comprehensive test case for cache operations
|
||||||
type CacheTestCase struct {
|
type CacheTestCase struct {
|
||||||
|
setup func(*TestFramework)
|
||||||
|
execute func(*TestFramework) error
|
||||||
|
validate func(*testing.T, error, *TestFramework)
|
||||||
|
cleanup func(*TestFramework)
|
||||||
name string
|
name string
|
||||||
cacheType string // "universal", "metadata", "bounded"
|
cacheType string
|
||||||
operation string // "get", "set", "evict", "cleanup"
|
operation string
|
||||||
setup func(*TestFramework) // Pre-test setup
|
skipReason string
|
||||||
execute func(*TestFramework) error // Test execution
|
timeout time.Duration
|
||||||
validate func(*testing.T, error, *TestFramework) // Validation logic
|
parallel bool
|
||||||
cleanup func(*TestFramework) // Post-test cleanup
|
|
||||||
timeout time.Duration // Test timeout
|
|
||||||
parallel bool // Can run in parallel
|
|
||||||
skipReason string // Optional reason to skip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// createTestCacheConfig creates a standard test configuration
|
// createTestCacheConfig creates a standard test configuration
|
||||||
@@ -698,10 +698,10 @@ func TestUnifiedCache_SetMaxSize(t *testing.T) {
|
|||||||
|
|
||||||
func TestNewCacheAdapter(t *testing.T) {
|
func TestNewCacheAdapter(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
cache interface{}
|
cache interface{}
|
||||||
expectNil bool
|
name string
|
||||||
description string
|
description string
|
||||||
|
expectNil bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "UniversalCache",
|
name: "UniversalCache",
|
||||||
|
|||||||
@@ -16,35 +16,26 @@ import (
|
|||||||
|
|
||||||
// ClientRegistrationResponse represents the response from a successful client registration (RFC 7591)
|
// ClientRegistrationResponse represents the response from a successful client registration (RFC 7591)
|
||||||
type ClientRegistrationResponse struct {
|
type ClientRegistrationResponse struct {
|
||||||
// Required fields
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
|
|
||||||
// Conditional - only for confidential clients
|
|
||||||
ClientSecret string `json:"client_secret,omitempty"`
|
|
||||||
|
|
||||||
// Optional - for managing registration
|
|
||||||
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
|
|
||||||
RegistrationClientURI string `json:"registration_client_uri,omitempty"`
|
|
||||||
|
|
||||||
// Expiration
|
|
||||||
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
|
|
||||||
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
|
|
||||||
|
|
||||||
// Echo back of registered metadata
|
|
||||||
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
|
||||||
ResponseTypes []string `json:"response_types,omitempty"`
|
|
||||||
GrantTypes []string `json:"grant_types,omitempty"`
|
|
||||||
ApplicationType string `json:"application_type,omitempty"`
|
|
||||||
Contacts []string `json:"contacts,omitempty"`
|
|
||||||
ClientName string `json:"client_name,omitempty"`
|
|
||||||
LogoURI string `json:"logo_uri,omitempty"`
|
|
||||||
ClientURI string `json:"client_uri,omitempty"`
|
|
||||||
PolicyURI string `json:"policy_uri,omitempty"`
|
|
||||||
TOSURI string `json:"tos_uri,omitempty"`
|
|
||||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
|
||||||
SubjectType string `json:"subject_type,omitempty"`
|
SubjectType string `json:"subject_type,omitempty"`
|
||||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
LogoURI string `json:"logo_uri,omitempty"`
|
||||||
|
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
|
||||||
|
RegistrationClientURI string `json:"registration_client_uri,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
||||||
|
TOSURI string `json:"tos_uri,omitempty"`
|
||||||
|
PolicyURI string `json:"policy_uri,omitempty"`
|
||||||
|
ClientSecret string `json:"client_secret,omitempty"`
|
||||||
|
ApplicationType string `json:"application_type,omitempty"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientName string `json:"client_name,omitempty"`
|
||||||
|
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||||
|
ClientURI string `json:"client_uri,omitempty"`
|
||||||
|
Contacts []string `json:"contacts,omitempty"`
|
||||||
|
GrantTypes []string `json:"grant_types,omitempty"`
|
||||||
|
ResponseTypes []string `json:"response_types,omitempty"`
|
||||||
|
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
||||||
|
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
|
||||||
|
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientRegistrationError represents an error response from client registration (RFC 7591)
|
// ClientRegistrationError represents an error response from client registration (RFC 7591)
|
||||||
@@ -55,14 +46,12 @@ type ClientRegistrationError struct {
|
|||||||
|
|
||||||
// DynamicClientRegistrar handles OIDC Dynamic Client Registration (RFC 7591)
|
// DynamicClientRegistrar handles OIDC Dynamic Client Registration (RFC 7591)
|
||||||
type DynamicClientRegistrar struct {
|
type DynamicClientRegistrar struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
logger *Logger
|
logger *Logger
|
||||||
config *DynamicClientRegistrationConfig
|
config *DynamicClientRegistrationConfig
|
||||||
providerURL string
|
|
||||||
|
|
||||||
// Cached registration response
|
|
||||||
mu sync.RWMutex
|
|
||||||
registrationResponse *ClientRegistrationResponse
|
registrationResponse *ClientRegistrationResponse
|
||||||
|
providerURL string
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDynamicClientRegistrar creates a new dynamic client registrar
|
// NewDynamicClientRegistrar creates a new dynamic client registrar
|
||||||
|
|||||||
@@ -223,10 +223,10 @@ func TestRegisterClientWithInitialAccessToken(t *testing.T) {
|
|||||||
// TestRegisterClientError tests error handling during registration
|
// TestRegisterClientError tests error handling during registration
|
||||||
func TestRegisterClientError(t *testing.T) {
|
func TestRegisterClientError(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
serverResponse func(w http.ResponseWriter, r *http.Request)
|
serverResponse func(w http.ResponseWriter, r *http.Request)
|
||||||
expectError bool
|
name string
|
||||||
errorContains string
|
errorContains string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "invalid_redirect_uri error",
|
name: "invalid_redirect_uri error",
|
||||||
@@ -321,8 +321,8 @@ func TestRegisterClientError(t *testing.T) {
|
|||||||
// TestRegisterClientDisabled tests that registration fails when not enabled
|
// TestRegisterClientDisabled tests that registration fails when not enabled
|
||||||
func TestRegisterClientDisabled(t *testing.T) {
|
func TestRegisterClientDisabled(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
dcrConfig *DynamicClientRegistrationConfig
|
dcrConfig *DynamicClientRegistrationConfig
|
||||||
|
name string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "nil config",
|
name: "nil config",
|
||||||
@@ -521,8 +521,8 @@ func TestCredentialsValidation(t *testing.T) {
|
|||||||
registrar := NewDynamicClientRegistrar(&http.Client{}, NewLogger("DEBUG"), dcrConfig, "https://example.com")
|
registrar := NewDynamicClientRegistrar(&http.Client{}, NewLogger("DEBUG"), dcrConfig, "https://example.com")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
response *ClientRegistrationResponse
|
response *ClientRegistrationResponse
|
||||||
|
name string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -584,9 +584,9 @@ func TestCredentialsValidation(t *testing.T) {
|
|||||||
// TestBuildRegistrationRequest tests the request body construction
|
// TestBuildRegistrationRequest tests the request body construction
|
||||||
func TestBuildRegistrationRequest(t *testing.T) {
|
func TestBuildRegistrationRequest(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
metadata *ClientRegistrationMetadata
|
metadata *ClientRegistrationMetadata
|
||||||
expectedFields map[string]interface{}
|
expectedFields map[string]interface{}
|
||||||
|
name string
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
|||||||
+29
-47
@@ -12,23 +12,19 @@ import (
|
|||||||
|
|
||||||
// EnhancedMockJWKCache is an improved state-based mock with call tracking
|
// EnhancedMockJWKCache is an improved state-based mock with call tracking
|
||||||
type EnhancedMockJWKCache struct {
|
type EnhancedMockJWKCache struct {
|
||||||
mu sync.RWMutex
|
Err error
|
||||||
|
JWKS *JWKSet
|
||||||
// State (what to return)
|
|
||||||
JWKS *JWKSet
|
|
||||||
Err error
|
|
||||||
|
|
||||||
// Call tracking
|
|
||||||
GetJWKSCalls []JWKSCall
|
GetJWKSCalls []JWKSCall
|
||||||
|
mu sync.RWMutex
|
||||||
|
getJWKSCallsMu sync.Mutex
|
||||||
CleanupCalls int32
|
CleanupCalls int32
|
||||||
CloseCalls int32
|
CloseCalls int32
|
||||||
getJWKSCallsMu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWKSCall records parameters from a GetJWKS call
|
// JWKSCall records parameters from a GetJWKS call
|
||||||
type JWKSCall struct {
|
type JWKSCall struct {
|
||||||
URL string
|
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
|
URL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *EnhancedMockJWKCache) GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error) {
|
func (m *EnhancedMockJWKCache) GetJWKS(ctx context.Context, jwksURL string, httpClient *http.Client) (*JWKSet, error) {
|
||||||
@@ -108,22 +104,18 @@ func (m *EnhancedMockJWKCache) Reset() {
|
|||||||
|
|
||||||
// EnhancedMockTokenVerifier is an improved state-based mock with call tracking
|
// EnhancedMockTokenVerifier is an improved state-based mock with call tracking
|
||||||
type EnhancedMockTokenVerifier struct {
|
type EnhancedMockTokenVerifier struct {
|
||||||
mu sync.RWMutex
|
Err error
|
||||||
|
VerifyFunc func(token string) error
|
||||||
// State (what to return) - can be a fixed error or a function
|
|
||||||
Err error
|
|
||||||
VerifyFunc func(token string) error
|
|
||||||
|
|
||||||
// Call tracking
|
|
||||||
VerifyCalls []TokenVerifyCall
|
VerifyCalls []TokenVerifyCall
|
||||||
|
mu sync.RWMutex
|
||||||
verifyCallsMu sync.Mutex
|
verifyCallsMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenVerifyCall records parameters from a VerifyToken call
|
// TokenVerifyCall records parameters from a VerifyToken call
|
||||||
type TokenVerifyCall struct {
|
type TokenVerifyCall struct {
|
||||||
Token string
|
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
Result error
|
Result error
|
||||||
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *EnhancedMockTokenVerifier) VerifyToken(token string) error {
|
func (m *EnhancedMockTokenVerifier) VerifyToken(token string) error {
|
||||||
@@ -207,49 +199,43 @@ func (m *EnhancedMockTokenVerifier) Reset() {
|
|||||||
|
|
||||||
// EnhancedMockTokenExchanger is an improved state-based mock with call tracking
|
// EnhancedMockTokenExchanger is an improved state-based mock with call tracking
|
||||||
type EnhancedMockTokenExchanger struct {
|
type EnhancedMockTokenExchanger struct {
|
||||||
mu sync.RWMutex
|
|
||||||
|
|
||||||
// State (what to return)
|
|
||||||
ExchangeResponse *TokenResponse
|
|
||||||
ExchangeErr error
|
|
||||||
RefreshResponse *TokenResponse
|
|
||||||
RefreshErr error
|
RefreshErr error
|
||||||
RevokeErr error
|
RevokeErr error
|
||||||
|
ExchangeErr error
|
||||||
// Optional functions for dynamic behavior
|
|
||||||
ExchangeCodeFunc func(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error)
|
ExchangeCodeFunc func(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error)
|
||||||
|
RefreshResponse *TokenResponse
|
||||||
|
ExchangeResponse *TokenResponse
|
||||||
RefreshTokenFunc func(refreshToken string) (*TokenResponse, error)
|
RefreshTokenFunc func(refreshToken string) (*TokenResponse, error)
|
||||||
RevokeTokenFunc func(token, tokenType string) error
|
RevokeTokenFunc func(token, tokenType string) error
|
||||||
|
ExchangeCalls []ExchangeCall
|
||||||
// Call tracking
|
RefreshCalls []RefreshCall
|
||||||
ExchangeCalls []ExchangeCall
|
RevokeCalls []RevokeCall
|
||||||
RefreshCalls []RefreshCall
|
mu sync.RWMutex
|
||||||
RevokeCalls []RevokeCall
|
exchangeCallsMu sync.Mutex
|
||||||
exchangeCallsMu sync.Mutex
|
refreshCallsMu sync.Mutex
|
||||||
refreshCallsMu sync.Mutex
|
revokeCallsMu sync.Mutex
|
||||||
revokeCallsMu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExchangeCall records parameters from an ExchangeCodeForToken call
|
// ExchangeCall records parameters from an ExchangeCodeForToken call
|
||||||
type ExchangeCall struct {
|
type ExchangeCall struct {
|
||||||
|
Timestamp time.Time
|
||||||
GrantType string
|
GrantType string
|
||||||
CodeOrToken string
|
CodeOrToken string
|
||||||
RedirectURL string
|
RedirectURL string
|
||||||
CodeVerifier string
|
CodeVerifier string
|
||||||
Timestamp time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshCall records parameters from a GetNewTokenWithRefreshToken call
|
// RefreshCall records parameters from a GetNewTokenWithRefreshToken call
|
||||||
type RefreshCall struct {
|
type RefreshCall struct {
|
||||||
RefreshToken string
|
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
|
RefreshToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevokeCall records parameters from a RevokeTokenWithProvider call
|
// RevokeCall records parameters from a RevokeTokenWithProvider call
|
||||||
type RevokeCall struct {
|
type RevokeCall struct {
|
||||||
|
Timestamp time.Time
|
||||||
Token string
|
Token string
|
||||||
TokenType string
|
TokenType string
|
||||||
Timestamp time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *EnhancedMockTokenExchanger) ExchangeCodeForToken(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) {
|
func (m *EnhancedMockTokenExchanger) ExchangeCodeForToken(ctx context.Context, grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error) {
|
||||||
@@ -401,16 +387,12 @@ func (m *EnhancedMockTokenExchanger) Reset() {
|
|||||||
|
|
||||||
// EnhancedMockCacheInterface is an improved state-based mock for CacheInterface
|
// EnhancedMockCacheInterface is an improved state-based mock for CacheInterface
|
||||||
type EnhancedMockCacheInterface struct {
|
type EnhancedMockCacheInterface struct {
|
||||||
mu sync.RWMutex
|
data map[string]cacheEntry
|
||||||
|
|
||||||
// Internal storage
|
|
||||||
data map[string]cacheEntry
|
|
||||||
maxSize int
|
|
||||||
|
|
||||||
// Call tracking
|
|
||||||
GetCalls []CacheGetCall
|
GetCalls []CacheGetCall
|
||||||
SetCalls []CacheSetCall
|
SetCalls []CacheSetCall
|
||||||
DeleteCalls []string
|
DeleteCalls []string
|
||||||
|
maxSize int
|
||||||
|
mu sync.RWMutex
|
||||||
getCalls sync.Mutex
|
getCalls sync.Mutex
|
||||||
setCalls sync.Mutex
|
setCalls sync.Mutex
|
||||||
deleteCalls sync.Mutex
|
deleteCalls sync.Mutex
|
||||||
@@ -423,17 +405,17 @@ type cacheEntry struct {
|
|||||||
|
|
||||||
// CacheGetCall records parameters from a Get call
|
// CacheGetCall records parameters from a Get call
|
||||||
type CacheGetCall struct {
|
type CacheGetCall struct {
|
||||||
|
Timestamp time.Time
|
||||||
Key string
|
Key string
|
||||||
Found bool
|
Found bool
|
||||||
Timestamp time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheSetCall records parameters from a Set call
|
// CacheSetCall records parameters from a Set call
|
||||||
type CacheSetCall struct {
|
type CacheSetCall struct {
|
||||||
Key string
|
|
||||||
Value any
|
|
||||||
TTL time.Duration
|
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
|
Value any
|
||||||
|
Key string
|
||||||
|
TTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEnhancedMockCache creates a new enhanced cache mock
|
// NewEnhancedMockCache creates a new enhanced cache mock
|
||||||
|
|||||||
+15
-36
@@ -642,14 +642,10 @@ func (e *HTTPError) Error() string {
|
|||||||
// OIDCError represents OIDC-specific errors with context information.
|
// OIDCError represents OIDC-specific errors with context information.
|
||||||
// It provides structured error reporting for authentication and authorization failures.
|
// It provides structured error reporting for authentication and authorization failures.
|
||||||
type OIDCError struct {
|
type OIDCError struct {
|
||||||
// Code identifies the specific error type
|
Cause error
|
||||||
Code string
|
|
||||||
// Message provides a human-readable description
|
|
||||||
Message string
|
|
||||||
// Context contains additional error context (e.g., provider, session details)
|
|
||||||
Context map[string]interface{}
|
Context map[string]interface{}
|
||||||
// Cause is the underlying error that caused this error
|
Code string
|
||||||
Cause error
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns the string representation of the OIDC error.
|
// Error returns the string representation of the OIDC error.
|
||||||
@@ -669,14 +665,10 @@ func (e *OIDCError) Unwrap() error {
|
|||||||
// SessionError represents session-related errors with context.
|
// SessionError represents session-related errors with context.
|
||||||
// Used for session management, validation, and storage errors.
|
// Used for session management, validation, and storage errors.
|
||||||
type SessionError struct {
|
type SessionError struct {
|
||||||
// Operation describes what session operation failed
|
Cause error
|
||||||
Operation string
|
Operation string
|
||||||
// Message provides a human-readable description
|
Message string
|
||||||
Message string
|
|
||||||
// SessionID identifies the session (if available)
|
|
||||||
SessionID string
|
SessionID string
|
||||||
// Cause is the underlying error that caused this error
|
|
||||||
Cause error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns the string representation of the session error.
|
// Error returns the string representation of the session error.
|
||||||
@@ -696,14 +688,10 @@ func (e *SessionError) Unwrap() error {
|
|||||||
// TokenError represents token-related errors with validation context.
|
// TokenError represents token-related errors with validation context.
|
||||||
// Used for JWT validation, token refresh, and token format errors.
|
// Used for JWT validation, token refresh, and token format errors.
|
||||||
type TokenError struct {
|
type TokenError struct {
|
||||||
// TokenType identifies the type of token (id_token, access_token, refresh_token)
|
Cause error
|
||||||
TokenType string
|
TokenType string
|
||||||
// Reason describes why the token is invalid
|
Reason string
|
||||||
Reason string
|
Message 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.
|
// Error returns the string representation of the token error.
|
||||||
@@ -765,24 +753,15 @@ func NewTokenError(tokenType, reason, message string, cause error) *TokenError {
|
|||||||
// It provides fallback mechanisms when primary services are unavailable and monitors
|
// It provides fallback mechanisms when primary services are unavailable and monitors
|
||||||
// service health to automatically recover when services become available again.
|
// service health to automatically recover when services become available again.
|
||||||
type GracefulDegradation struct {
|
type GracefulDegradation struct {
|
||||||
// BaseRecoveryMechanism provides common functionality
|
|
||||||
*BaseRecoveryMechanism
|
*BaseRecoveryMechanism
|
||||||
// fallbacks stores service-specific fallback implementations
|
fallbacks map[string]func() (interface{}, error)
|
||||||
fallbacks map[string]func() (interface{}, error)
|
healthChecks map[string]func() bool
|
||||||
// healthChecks stores service health check functions
|
|
||||||
healthChecks map[string]func() bool
|
|
||||||
// degradedServices tracks which services are currently degraded
|
|
||||||
degradedServices map[string]time.Time
|
degradedServices map[string]time.Time
|
||||||
// config contains graceful degradation configuration
|
healthCheckTask *BackgroundTask
|
||||||
config GracefulDegradationConfig
|
stopChan chan struct{}
|
||||||
// mutex protects shared state
|
config GracefulDegradationConfig
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
// healthCheckTask manages background health checking
|
shutdownOnce sync.Once
|
||||||
healthCheckTask *BackgroundTask
|
|
||||||
// stopChan signals shutdown
|
|
||||||
stopChan chan struct{}
|
|
||||||
// shutdownOnce ensures shutdown happens only once
|
|
||||||
shutdownOnce sync.Once
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GracefulDegradationConfig holds configuration for graceful degradation behavior.
|
// GracefulDegradationConfig holds configuration for graceful degradation behavior.
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ import (
|
|||||||
func TestCircuitBreakerStateTransitions(t *testing.T) {
|
func TestCircuitBreakerStateTransitions(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
failures int
|
|
||||||
maxFailures int
|
|
||||||
expectedStateBefore string
|
expectedStateBefore string
|
||||||
expectedStateAfter string
|
expectedStateAfter string
|
||||||
|
failures int
|
||||||
|
maxFailures int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "stays closed below threshold",
|
name: "stays closed below threshold",
|
||||||
@@ -543,8 +543,8 @@ func TestRetryExecutorNetworkErrors(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
err error
|
err error
|
||||||
|
name string
|
||||||
shouldRetry bool
|
shouldRetry bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -1647,8 +1647,8 @@ func TestGracefulDegradationFullScenario(t *testing.T) {
|
|||||||
|
|
||||||
func TestIsTraefikDefaultCertError(t *testing.T) {
|
func TestIsTraefikDefaultCertError(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
err error
|
err error
|
||||||
|
name string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -1680,8 +1680,8 @@ func TestIsTraefikDefaultCertError(t *testing.T) {
|
|||||||
|
|
||||||
func TestIsEOFError(t *testing.T) {
|
func TestIsEOFError(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
err error
|
err error
|
||||||
|
name string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -1723,8 +1723,8 @@ func TestIsEOFError(t *testing.T) {
|
|||||||
|
|
||||||
func TestIsCertificateError(t *testing.T) {
|
func TestIsCertificateError(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
err error
|
err error
|
||||||
|
name string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -1811,8 +1811,8 @@ func TestRetryExecutorStartupErrors(t *testing.T) {
|
|||||||
_ = NewRetryExecutor(MetadataFetchRetryConfig(), nil)
|
_ = NewRetryExecutor(MetadataFetchRetryConfig(), nil)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
err error
|
err error
|
||||||
|
name string
|
||||||
shouldRetry bool
|
shouldRetry bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -1890,8 +1890,8 @@ func TestRetryExecutorIsRetryableErrorIntegration(t *testing.T) {
|
|||||||
re := NewRetryExecutor(DefaultRetryConfig(), nil)
|
re := NewRetryExecutor(DefaultRetryConfig(), nil)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
err error
|
err error
|
||||||
|
name string
|
||||||
shouldRetry bool
|
shouldRetry bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -1977,9 +1977,9 @@ func circuitBreakerStateToString(state CircuitBreakerState) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mockNetError struct {
|
type mockNetError struct {
|
||||||
|
msg string
|
||||||
timeout bool
|
timeout bool
|
||||||
temporary bool
|
temporary bool
|
||||||
msg string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *mockNetError) Error() string { return e.msg }
|
func (e *mockNetError) Error() string { return e.msg }
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ import (
|
|||||||
type GoroutineManager struct {
|
type GoroutineManager struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
wg sync.WaitGroup
|
|
||||||
mu sync.RWMutex
|
|
||||||
goroutines map[string]*managedGoroutine
|
goroutines map[string]*managedGoroutine
|
||||||
logger *Logger
|
logger *Logger
|
||||||
|
wg sync.WaitGroup
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type managedGoroutine struct {
|
type managedGoroutine struct {
|
||||||
name string
|
|
||||||
cancel context.CancelFunc
|
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
|
cancel context.CancelFunc
|
||||||
|
name string
|
||||||
running bool
|
running bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,10 +149,10 @@ func (m *GoroutineManager) GetStatus() map[string]GoroutineStatus {
|
|||||||
|
|
||||||
// GoroutineStatus represents the status of a managed goroutine
|
// GoroutineStatus represents the status of a managed goroutine
|
||||||
type GoroutineStatus struct {
|
type GoroutineStatus struct {
|
||||||
Name string
|
|
||||||
Running bool
|
|
||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
|
Name string
|
||||||
Runtime time.Duration
|
Runtime time.Duration
|
||||||
|
Running bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrShutdownTimeout is returned when shutdown times out
|
// ErrShutdownTimeout is returned when shutdown times out
|
||||||
|
|||||||
+12
-19
@@ -12,30 +12,23 @@ import (
|
|||||||
|
|
||||||
// HTTPClientConfig provides configuration for creating HTTP clients
|
// HTTPClientConfig provides configuration for creating HTTP clients
|
||||||
type HTTPClientConfig struct {
|
type HTTPClientConfig struct {
|
||||||
// Timeout for the entire request
|
IdleConnTimeout time.Duration
|
||||||
Timeout time.Duration
|
MaxIdleConns int
|
||||||
// MaxRedirects allowed (0 means follow Go's default of 10)
|
ReadBufferSize int
|
||||||
MaxRedirects int
|
|
||||||
// UseCookieJar enables cookie jar for the client
|
|
||||||
UseCookieJar bool
|
|
||||||
// Connection settings
|
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
KeepAlive time.Duration
|
KeepAlive time.Duration
|
||||||
TLSHandshakeTimeout time.Duration
|
TLSHandshakeTimeout time.Duration
|
||||||
ResponseHeaderTimeout time.Duration
|
ResponseHeaderTimeout time.Duration
|
||||||
ExpectContinueTimeout time.Duration
|
ExpectContinueTimeout time.Duration
|
||||||
IdleConnTimeout time.Duration
|
MaxRedirects int
|
||||||
// Connection pool settings
|
MaxIdleConnsPerHost int
|
||||||
MaxIdleConns int
|
Timeout time.Duration
|
||||||
MaxIdleConnsPerHost int
|
MaxConnsPerHost int
|
||||||
MaxConnsPerHost int
|
WriteBufferSize int
|
||||||
// Buffer settings
|
UseCookieJar bool
|
||||||
WriteBufferSize int
|
ForceHTTP2 bool
|
||||||
ReadBufferSize int
|
DisableKeepAlives bool
|
||||||
// Feature flags
|
DisableCompression bool
|
||||||
ForceHTTP2 bool
|
|
||||||
DisableKeepAlives bool
|
|
||||||
DisableCompression bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultHTTPClientConfig returns the default configuration for general use
|
// DefaultHTTPClientConfig returns the default configuration for general use
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ func TestHTTPClientFactoryValidateHTTPClientConfig(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
errorMsg string
|
||||||
config HTTPClientConfig
|
config HTTPClientConfig
|
||||||
wantError bool
|
wantError bool
|
||||||
errorMsg string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid config",
|
name: "valid config",
|
||||||
|
|||||||
+6
-6
@@ -12,19 +12,19 @@ import (
|
|||||||
|
|
||||||
// SharedTransportPool manages a pool of shared HTTP transports to prevent connection exhaustion
|
// SharedTransportPool manages a pool of shared HTTP transports to prevent connection exhaustion
|
||||||
type SharedTransportPool struct {
|
type SharedTransportPool struct {
|
||||||
mu sync.RWMutex
|
|
||||||
transports map[string]*sharedTransport
|
|
||||||
maxConns int
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
transports map[string]*sharedTransport
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
clientCount int32 // SECURITY FIX: Track total HTTP clients
|
maxConns int
|
||||||
maxClients int32 // SECURITY FIX: Limit total clients to 5
|
mu sync.RWMutex
|
||||||
|
clientCount int32
|
||||||
|
maxClients int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type sharedTransport struct {
|
type sharedTransport struct {
|
||||||
|
lastUsed time.Time
|
||||||
transport *http.Transport
|
transport *http.Transport
|
||||||
refCount int
|
refCount int
|
||||||
lastUsed time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func TestInputValidator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Valid token validation", func(t *testing.T) {
|
t.Run("Valid token validation", func(t *testing.T) {
|
||||||
validToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHs3UjpMC6M6FNqI2J-I2NxrragtnDxGxdJUvDERDQVHzeNlVQiuqWDEeO_O-0KptafbfyuGqfQxH_6dp2_MeFpAc"
|
validToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHs3UjpMC6M6FNqI2J-I2NxrragtnDxGxdJUvDERDQVHzeNlVQiuqWDEeO_O-0KptafbfyuGqfQxH_6dp2_MeFpAc" // trufflehog:ignore
|
||||||
|
|
||||||
result := validator.ValidateToken(validToken)
|
result := validator.ValidateToken(validToken)
|
||||||
if !result.IsValid {
|
if !result.IsValid {
|
||||||
@@ -428,12 +428,12 @@ func TestInputValidatorValidateToken(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
token string
|
token string
|
||||||
expectValid bool
|
|
||||||
description string
|
description string
|
||||||
|
expectValid bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "ValidJWTToken",
|
name: "ValidJWTToken",
|
||||||
token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyLCJpYXQiOjE1MTYyMzkwMjJ9.signature",
|
token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyLCJpYXQiOjE1MTYyMzkwMjJ9.signature", // trufflehog:ignore
|
||||||
expectValid: true,
|
expectValid: true,
|
||||||
description: "Valid JWT token should pass validation",
|
description: "Valid JWT token should pass validation",
|
||||||
},
|
},
|
||||||
@@ -475,7 +475,7 @@ func TestInputValidatorValidateToken(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "MaliciousJWTWithExtraData",
|
name: "MaliciousJWTWithExtraData",
|
||||||
token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig.malicious_extra",
|
token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig.malicious_extra", // trufflehog:ignore
|
||||||
expectValid: false,
|
expectValid: false,
|
||||||
description: "JWT with extra malicious data should fail validation",
|
description: "JWT with extra malicious data should fail validation",
|
||||||
},
|
},
|
||||||
@@ -500,8 +500,8 @@ func TestInputValidatorValidateEmail(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
email string
|
email string
|
||||||
expectValid bool
|
|
||||||
description string
|
description string
|
||||||
|
expectValid bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "ValidEmail",
|
name: "ValidEmail",
|
||||||
@@ -578,8 +578,8 @@ func TestInputValidatorValidateURL(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
url string
|
url string
|
||||||
expectValid bool
|
|
||||||
description string
|
description string
|
||||||
|
expectValid bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "ValidHTTPSURL",
|
name: "ValidHTTPSURL",
|
||||||
@@ -669,8 +669,8 @@ func TestInputValidatorValidateClaim(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
claimName string
|
claimName string
|
||||||
claimValue string
|
claimValue string
|
||||||
expectValid bool
|
|
||||||
description string
|
description string
|
||||||
|
expectValid bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "ValidStringClaim",
|
name: "ValidStringClaim",
|
||||||
@@ -750,8 +750,8 @@ func TestInputValidatorValidateHeader(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
headerName string
|
headerName string
|
||||||
headerValue string
|
headerValue string
|
||||||
expectValid bool
|
|
||||||
description string
|
description string
|
||||||
|
expectValid bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "ValidHeader",
|
name: "ValidHeader",
|
||||||
@@ -830,8 +830,8 @@ func TestInputValidatorValidateUsername(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
username string
|
username string
|
||||||
expectValid bool
|
|
||||||
description string
|
description string
|
||||||
|
expectValid bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "ValidUsername",
|
name: "ValidUsername",
|
||||||
|
|||||||
@@ -726,20 +726,20 @@ type MockConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MockSession struct {
|
type MockSession struct {
|
||||||
id string
|
|
||||||
userID string
|
|
||||||
created time.Time
|
created time.Time
|
||||||
lastUsed time.Time
|
lastUsed time.Time
|
||||||
data map[string]interface{}
|
data map[string]interface{}
|
||||||
|
id string
|
||||||
|
userID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestResult struct {
|
type TestResult struct {
|
||||||
UserID int
|
|
||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
EndTime time.Time
|
EndTime time.Time
|
||||||
|
Error error
|
||||||
|
UserID int
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
Success bool
|
Success bool
|
||||||
Error error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Vendored
+14
-25
@@ -18,33 +18,22 @@ const (
|
|||||||
|
|
||||||
// Config provides common configuration for cache backends
|
// Config provides common configuration for cache backends
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Type specifies the backend type
|
L2Config *Config
|
||||||
Type BackendType
|
L1Config *Config
|
||||||
|
RedisPrefix string
|
||||||
// Memory backend settings
|
Type BackendType
|
||||||
MaxSize int
|
RedisAddr string
|
||||||
MaxMemoryBytes int64
|
RedisPassword string
|
||||||
CleanupInterval time.Duration
|
PoolSize int
|
||||||
|
RedisDB int
|
||||||
// Redis backend settings
|
CleanupInterval time.Duration
|
||||||
RedisAddr string
|
MaxMemoryBytes int64
|
||||||
RedisPassword string
|
MaxSize int
|
||||||
RedisDB int
|
HealthCheckInterval time.Duration
|
||||||
RedisPrefix string
|
AsyncWrites bool
|
||||||
PoolSize int
|
|
||||||
|
|
||||||
// Hybrid backend settings
|
|
||||||
L1Config *Config // Memory cache (L1)
|
|
||||||
L2Config *Config // Redis cache (L2)
|
|
||||||
AsyncWrites bool // Write to L2 asynchronously
|
|
||||||
|
|
||||||
// Resilience settings
|
|
||||||
EnableCircuitBreaker bool
|
EnableCircuitBreaker bool
|
||||||
EnableHealthCheck bool
|
EnableHealthCheck bool
|
||||||
HealthCheckInterval time.Duration
|
EnableMetrics bool
|
||||||
|
|
||||||
// Metrics
|
|
||||||
EnableMetrics bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns a default configuration for in-memory caching
|
// DefaultConfig returns a default configuration for in-memory caching
|
||||||
|
|||||||
Vendored
+18
-28
@@ -13,40 +13,30 @@ import (
|
|||||||
// HybridBackend implements a two-tier cache with L1 (memory) and L2 (Redis) backends
|
// HybridBackend implements a two-tier cache with L1 (memory) and L2 (Redis) backends
|
||||||
// It provides automatic failover, async writes for non-critical data, and optimized read paths
|
// It provides automatic failover, async writes for non-critical data, and optimized read paths
|
||||||
type HybridBackend struct {
|
type HybridBackend struct {
|
||||||
primary CacheBackend // L1: Memory cache for fast access
|
lastL2Error atomic.Value
|
||||||
secondary CacheBackend // L2: Redis cache for distributed access
|
secondary CacheBackend
|
||||||
|
primary CacheBackend
|
||||||
// Configuration
|
logger Logger
|
||||||
syncWriteCacheTypes map[string]bool // Which cache types require synchronous writes
|
ctx context.Context
|
||||||
|
syncWriteCacheTypes map[string]bool
|
||||||
asyncWriteBuffer chan *asyncWriteItem
|
asyncWriteBuffer chan *asyncWriteItem
|
||||||
|
cancel context.CancelFunc
|
||||||
// Metrics
|
wg sync.WaitGroup
|
||||||
l1Hits atomic.Int64
|
l1Hits atomic.Int64
|
||||||
l2Hits atomic.Int64
|
errors atomic.Int64
|
||||||
misses atomic.Int64
|
l2Writes atomic.Int64
|
||||||
l1Writes atomic.Int64
|
l1Writes atomic.Int64
|
||||||
l2Writes atomic.Int64
|
misses atomic.Int64
|
||||||
errors atomic.Int64
|
l2Hits atomic.Int64
|
||||||
|
fallbackMode atomic.Bool
|
||||||
// Fallback tracking
|
|
||||||
fallbackMode atomic.Bool // True when operating in degraded mode (L1 only)
|
|
||||||
lastL2Error atomic.Value // Stores last L2 error timestamp
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logger Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// asyncWriteItem represents an async write operation
|
// asyncWriteItem represents an async write operation
|
||||||
type asyncWriteItem struct {
|
type asyncWriteItem struct {
|
||||||
|
ctx context.Context
|
||||||
key string
|
key string
|
||||||
value []byte
|
value []byte
|
||||||
ttl time.Duration
|
ttl time.Duration
|
||||||
ctx context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger interface for structured logging
|
// Logger interface for structured logging
|
||||||
@@ -82,9 +72,9 @@ func (l *defaultLogger) Errorf(format string, args ...interface{}) {
|
|||||||
type HybridConfig struct {
|
type HybridConfig struct {
|
||||||
Primary CacheBackend
|
Primary CacheBackend
|
||||||
Secondary CacheBackend
|
Secondary CacheBackend
|
||||||
SyncWriteCacheTypes map[string]bool // Cache types requiring synchronous L2 writes
|
|
||||||
AsyncBufferSize int
|
|
||||||
Logger Logger
|
Logger Logger
|
||||||
|
SyncWriteCacheTypes map[string]bool
|
||||||
|
AsyncBufferSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHybridBackend creates a new hybrid cache backend with L1 (memory) and L2 (Redis) tiers
|
// NewHybridBackend creates a new hybrid cache backend with L1 (memory) and L2 (Redis) tiers
|
||||||
|
|||||||
+6
-6
@@ -17,23 +17,23 @@ import (
|
|||||||
|
|
||||||
// mockBackend is a simple mock implementation of CacheBackend for testing
|
// mockBackend is a simple mock implementation of CacheBackend for testing
|
||||||
type mockBackend struct {
|
type mockBackend struct {
|
||||||
|
pingError error
|
||||||
data map[string]mockEntry
|
data map[string]mockEntry
|
||||||
|
stats map[string]interface{}
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
getCalls atomic.Int32
|
||||||
|
setCalls atomic.Int32
|
||||||
|
deleteCalls atomic.Int32
|
||||||
failSet bool
|
failSet bool
|
||||||
failGet bool
|
failGet bool
|
||||||
failDelete bool
|
failDelete bool
|
||||||
failClear bool
|
failClear bool
|
||||||
failPing bool
|
failPing bool
|
||||||
pingError error
|
|
||||||
stats map[string]interface{}
|
|
||||||
getCalls atomic.Int32
|
|
||||||
setCalls atomic.Int32
|
|
||||||
deleteCalls atomic.Int32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockEntry struct {
|
type mockEntry struct {
|
||||||
value []byte
|
|
||||||
expiresAt time.Time
|
expiresAt time.Time
|
||||||
|
value []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// mockBatchBackend extends mockBackend with batch operations
|
// mockBatchBackend extends mockBackend with batch operations
|
||||||
|
|||||||
Vendored
+14
-45
@@ -41,53 +41,22 @@ type CacheBackend interface {
|
|||||||
|
|
||||||
// BackendStats represents statistics for a cache backend
|
// BackendStats represents statistics for a cache backend
|
||||||
type BackendStats struct {
|
type BackendStats struct {
|
||||||
// Type is the backend type
|
StartTime time.Time
|
||||||
Type BackendType
|
LastErrorTime time.Time
|
||||||
|
Type BackendType
|
||||||
// Hits is the number of cache hits
|
LastError string
|
||||||
Hits int64
|
Deletes int64
|
||||||
|
Errors int64
|
||||||
// Misses is the number of cache misses
|
Evictions int64
|
||||||
Misses int64
|
CurrentSize int64
|
||||||
|
MaxSize int64
|
||||||
// Sets is the number of set operations
|
MemoryUsage int64
|
||||||
Sets int64
|
|
||||||
|
|
||||||
// Deletes is the number of delete operations
|
|
||||||
Deletes int64
|
|
||||||
|
|
||||||
// Errors is the number of errors
|
|
||||||
Errors int64
|
|
||||||
|
|
||||||
// Evictions is the number of evicted items
|
|
||||||
Evictions int64
|
|
||||||
|
|
||||||
// CurrentSize is the current number of items in cache
|
|
||||||
CurrentSize int64
|
|
||||||
|
|
||||||
// MaxSize is the maximum number of items (0 means unlimited)
|
|
||||||
MaxSize int64
|
|
||||||
|
|
||||||
// MemoryUsage is the approximate memory usage in bytes
|
|
||||||
MemoryUsage int64
|
|
||||||
|
|
||||||
// AverageGetLatency is the average latency for get operations
|
|
||||||
AverageGetLatency time.Duration
|
AverageGetLatency time.Duration
|
||||||
|
|
||||||
// AverageSetLatency is the average latency for set operations
|
|
||||||
AverageSetLatency time.Duration
|
AverageSetLatency time.Duration
|
||||||
|
Sets int64
|
||||||
// LastError is the last error encountered
|
Misses int64
|
||||||
LastError string
|
Uptime time.Duration
|
||||||
|
Hits int64
|
||||||
// LastErrorTime is when the last error occurred
|
|
||||||
LastErrorTime time.Time
|
|
||||||
|
|
||||||
// Uptime is how long the backend has been running
|
|
||||||
Uptime time.Duration
|
|
||||||
|
|
||||||
// StartTime is when the backend was started
|
|
||||||
StartTime time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackendCapabilities describes the capabilities of a cache backend
|
// BackendCapabilities describes the capabilities of a cache backend
|
||||||
|
|||||||
Vendored
+27
-35
@@ -11,14 +11,14 @@ import (
|
|||||||
|
|
||||||
// memoryCacheItem represents an item in the memory cache
|
// memoryCacheItem represents an item in the memory cache
|
||||||
type memoryCacheItem struct {
|
type memoryCacheItem struct {
|
||||||
key string
|
|
||||||
value interface{}
|
|
||||||
expiresAt time.Time
|
expiresAt time.Time
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
accessedAt time.Time
|
accessedAt time.Time
|
||||||
|
value interface{}
|
||||||
|
element *list.Element
|
||||||
|
key string
|
||||||
accessCount int64
|
accessCount int64
|
||||||
size int64
|
size int64
|
||||||
element *list.Element // for LRU tracking
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isExpired checks if the item is expired
|
// isExpired checks if the item is expired
|
||||||
@@ -31,39 +31,31 @@ func (item *memoryCacheItem) isExpired() bool {
|
|||||||
|
|
||||||
// MemoryCacheBackend implements the CacheBackend interface using in-memory storage
|
// MemoryCacheBackend implements the CacheBackend interface using in-memory storage
|
||||||
type MemoryCacheBackend struct {
|
type MemoryCacheBackend struct {
|
||||||
mu sync.RWMutex
|
startTime time.Time
|
||||||
items map[string]*memoryCacheItem
|
lastErrorTime time.Time
|
||||||
lruList *list.List
|
items map[string]*memoryCacheItem
|
||||||
maxSize int64
|
lruList *list.List
|
||||||
maxMemory int64
|
cleanupDone chan bool
|
||||||
currentSize int64
|
cleanupTicker *time.Ticker
|
||||||
currentMemory int64
|
evictionPolicy string
|
||||||
|
lastError string
|
||||||
// Statistics
|
currentMemory int64
|
||||||
hits atomic.Int64
|
misses atomic.Int64
|
||||||
misses atomic.Int64
|
deletes atomic.Int64
|
||||||
sets atomic.Int64
|
evictions atomic.Int64
|
||||||
deletes atomic.Int64
|
errors atomic.Int64
|
||||||
evictions atomic.Int64
|
totalGetTime atomic.Int64
|
||||||
errors atomic.Int64
|
totalSetTime atomic.Int64
|
||||||
|
getCount atomic.Int64
|
||||||
// Latency tracking
|
setCount atomic.Int64
|
||||||
totalGetTime atomic.Int64
|
sets atomic.Int64
|
||||||
totalSetTime atomic.Int64
|
hits atomic.Int64
|
||||||
getCount atomic.Int64
|
maxSize int64
|
||||||
setCount atomic.Int64
|
currentSize int64
|
||||||
|
maxMemory int64
|
||||||
// Status
|
|
||||||
startTime time.Time
|
|
||||||
lastError string
|
|
||||||
lastErrorTime time.Time
|
|
||||||
cleanupTicker *time.Ticker
|
|
||||||
cleanupDone chan bool
|
|
||||||
closed atomic.Bool
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
cleanupInterval time.Duration
|
cleanupInterval time.Duration
|
||||||
evictionPolicy string // "lru", "lfu", "fifo"
|
mu sync.RWMutex
|
||||||
|
closed atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMemoryCacheBackend creates a new memory cache backend
|
// NewMemoryCacheBackend creates a new memory cache backend
|
||||||
|
|||||||
+10
-16
@@ -9,30 +9,24 @@ import (
|
|||||||
|
|
||||||
// HealthMonitor continuously monitors Redis connection health and triggers reconnections
|
// HealthMonitor continuously monitors Redis connection health and triggers reconnections
|
||||||
type HealthMonitor struct {
|
type HealthMonitor struct {
|
||||||
pool *ConnectionPool
|
pool *ConnectionPool
|
||||||
config *HealthMonitorConfig
|
config *HealthMonitorConfig
|
||||||
|
stopChan chan struct{}
|
||||||
// State
|
wg sync.WaitGroup
|
||||||
healthy atomic.Bool
|
lastCheckTime atomic.Int64
|
||||||
running atomic.Bool
|
|
||||||
lastCheckTime atomic.Int64 // Unix timestamp
|
|
||||||
|
|
||||||
// Metrics
|
|
||||||
consecutiveFailures atomic.Int64
|
consecutiveFailures atomic.Int64
|
||||||
totalChecks atomic.Int64
|
totalChecks atomic.Int64
|
||||||
totalFailures atomic.Int64
|
totalFailures atomic.Int64
|
||||||
|
healthy atomic.Bool
|
||||||
// Lifecycle
|
running atomic.Bool
|
||||||
stopChan chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthMonitorConfig configures the health monitor
|
// HealthMonitorConfig configures the health monitor
|
||||||
type HealthMonitorConfig struct {
|
type HealthMonitorConfig struct {
|
||||||
CheckInterval time.Duration // How often to check health
|
|
||||||
Timeout time.Duration // Timeout for health check
|
|
||||||
UnhealthyThreshold int // Consecutive failures before marking unhealthy
|
|
||||||
OnHealthChange func(healthy bool)
|
OnHealthChange func(healthy bool)
|
||||||
|
CheckInterval time.Duration
|
||||||
|
Timeout time.Duration
|
||||||
|
UnhealthyThreshold int
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultHealthMonitorConfig returns default health monitor configuration
|
// DefaultHealthMonitorConfig returns default health monitor configuration
|
||||||
|
|||||||
Vendored
+5
-5
@@ -15,8 +15,8 @@ import (
|
|||||||
func TestRESPWriter_WriteCommand(t *testing.T) {
|
func TestRESPWriter_WriteCommand(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args []string
|
|
||||||
expected string
|
expected string
|
||||||
|
args []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Simple command",
|
name: "Simple command",
|
||||||
@@ -205,9 +205,9 @@ func TestRESPReader_ReadInteger(t *testing.T) {
|
|||||||
// TestRESPReader_ReadBulkString tests reading bulk strings
|
// TestRESPReader_ReadBulkString tests reading bulk strings
|
||||||
func TestRESPReader_ReadBulkString(t *testing.T) {
|
func TestRESPReader_ReadBulkString(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
expected interface{}
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
expected interface{}
|
|
||||||
wantErr bool
|
wantErr bool
|
||||||
isNil bool
|
isNil bool
|
||||||
}{
|
}{
|
||||||
@@ -440,10 +440,10 @@ func TestRESPHelpers(t *testing.T) {
|
|||||||
// TestRESPRoundTrip tests full round-trip encoding/decoding
|
// TestRESPRoundTrip tests full round-trip encoding/decoding
|
||||||
func TestRESPRoundTrip(t *testing.T) {
|
func TestRESPRoundTrip(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
command []string
|
|
||||||
response string
|
|
||||||
expected interface{}
|
expected interface{}
|
||||||
|
name string
|
||||||
|
response string
|
||||||
|
command []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "PING command",
|
name: "PING command",
|
||||||
|
|||||||
Vendored
+28
-40
@@ -33,21 +33,19 @@ type Logger interface {
|
|||||||
|
|
||||||
// Config provides configuration for the cache
|
// Config provides configuration for the cache
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Logger Logger
|
||||||
|
JWKConfig *JWKConfig
|
||||||
|
MetadataConfig *MetadataConfig
|
||||||
|
TokenConfig *TokenConfig
|
||||||
Type Type
|
Type Type
|
||||||
MaxSize int
|
|
||||||
MaxMemoryBytes int64
|
|
||||||
DefaultTTL time.Duration
|
DefaultTTL time.Duration
|
||||||
CleanupInterval time.Duration
|
CleanupInterval time.Duration
|
||||||
EnableCompression bool
|
MaxMemoryBytes int64
|
||||||
|
MaxSize int
|
||||||
EnableMetrics bool
|
EnableMetrics bool
|
||||||
EnableAutoCleanup bool
|
EnableAutoCleanup bool
|
||||||
EnableMemoryLimit bool
|
EnableMemoryLimit bool
|
||||||
Logger Logger
|
EnableCompression bool
|
||||||
|
|
||||||
// Type-specific configurations
|
|
||||||
TokenConfig *TokenConfig
|
|
||||||
MetadataConfig *MetadataConfig
|
|
||||||
JWKConfig *JWKConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenConfig provides token-specific cache configuration
|
// TokenConfig provides token-specific cache configuration
|
||||||
@@ -59,11 +57,11 @@ type TokenConfig struct {
|
|||||||
|
|
||||||
// MetadataConfig provides metadata-specific cache configuration
|
// MetadataConfig provides metadata-specific cache configuration
|
||||||
type MetadataConfig struct {
|
type MetadataConfig struct {
|
||||||
|
SecurityCriticalFields []string
|
||||||
GracePeriod time.Duration
|
GracePeriod time.Duration
|
||||||
ExtendedGracePeriod time.Duration
|
ExtendedGracePeriod time.Duration
|
||||||
MaxGracePeriod time.Duration
|
MaxGracePeriod time.Duration
|
||||||
SecurityCriticalMaxGracePeriod time.Duration
|
SecurityCriticalMaxGracePeriod time.Duration
|
||||||
SecurityCriticalFields []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWKConfig provides JWK-specific cache configuration
|
// JWKConfig provides JWK-specific cache configuration
|
||||||
@@ -75,45 +73,35 @@ type JWKConfig struct {
|
|||||||
|
|
||||||
// Item represents a single cache entry
|
// Item represents a single cache entry
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Key string
|
|
||||||
Value interface{}
|
|
||||||
Size int64
|
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
LastAccessed time.Time
|
LastAccessed time.Time
|
||||||
AccessCount int64
|
Value interface{}
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
element *list.Element
|
||||||
|
Key string
|
||||||
CacheType Type
|
CacheType Type
|
||||||
|
Size int64
|
||||||
// Type-specific metadata
|
AccessCount int64
|
||||||
Metadata map[string]interface{}
|
|
||||||
|
|
||||||
// LRU list element reference
|
|
||||||
element *list.Element
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache provides a single, unified cache implementation
|
// Cache provides a single, unified cache implementation
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
mu sync.RWMutex
|
config Config
|
||||||
items map[string]*Item
|
ctx context.Context
|
||||||
lruList *list.List
|
logger Logger
|
||||||
config Config
|
cancel context.CancelFunc
|
||||||
logger Logger
|
lruList *list.List
|
||||||
|
items map[string]*Item
|
||||||
// Memory management
|
stopCleanup chan bool
|
||||||
|
wg sync.WaitGroup
|
||||||
currentSize int64
|
currentSize int64
|
||||||
currentMemory int64
|
currentMemory int64
|
||||||
|
hits int64
|
||||||
// Metrics
|
misses int64
|
||||||
hits int64
|
evictions int64
|
||||||
misses int64
|
sets int64
|
||||||
evictions int64
|
mu sync.RWMutex
|
||||||
sets int64
|
closed int32
|
||||||
|
|
||||||
// Lifecycle management
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
wg sync.WaitGroup
|
|
||||||
stopCleanup chan bool
|
|
||||||
closed int32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns a default cache configuration
|
// DefaultConfig returns a default cache configuration
|
||||||
|
|||||||
Vendored
+11
-11
@@ -1750,19 +1750,19 @@ func TestAdvancedEdgeCases(t *testing.T) {
|
|||||||
|
|
||||||
// Test with various data types
|
// Test with various data types
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
key string
|
|
||||||
value interface{}
|
value interface{}
|
||||||
|
key string
|
||||||
}{
|
}{
|
||||||
{"string", "test string"},
|
{key: "string", value: "test string"},
|
||||||
{"int", 42},
|
{key: "int", value: 42},
|
||||||
{"float", 3.14159},
|
{key: "float", value: 3.14159},
|
||||||
{"bool", true},
|
{key: "bool", value: true},
|
||||||
{"slice", []string{"a", "b", "c"}},
|
{key: "slice", value: []string{"a", "b", "c"}},
|
||||||
{"map", map[string]int{"one": 1, "two": 2}},
|
{key: "map", value: map[string]int{"one": 1, "two": 2}},
|
||||||
{"nil", nil},
|
{key: "nil", value: nil},
|
||||||
{"empty-string", ""},
|
{key: "empty-string", value: ""},
|
||||||
{"empty-slice", []string{}},
|
{key: "empty-slice", value: []string{}},
|
||||||
{"empty-map", map[string]interface{}{}},
|
{key: "empty-map", value: map[string]interface{}{}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|||||||
Vendored
+2
-7
@@ -7,22 +7,17 @@ import (
|
|||||||
|
|
||||||
// Manager manages multiple cache instances with singleton pattern
|
// Manager manages multiple cache instances with singleton pattern
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.RWMutex
|
logger Logger
|
||||||
|
|
||||||
// Core caches
|
|
||||||
tokenCache *Cache
|
tokenCache *Cache
|
||||||
metadataCache *Cache
|
metadataCache *Cache
|
||||||
jwkCache *Cache
|
jwkCache *Cache
|
||||||
sessionCache *Cache
|
sessionCache *Cache
|
||||||
generalCache *Cache
|
generalCache *Cache
|
||||||
|
|
||||||
// Typed wrappers
|
|
||||||
typedToken *TokenCache
|
typedToken *TokenCache
|
||||||
typedMetadata *MetadataCache
|
typedMetadata *MetadataCache
|
||||||
typedJWK *JWKCache
|
typedJWK *JWKCache
|
||||||
typedSession *SessionCache
|
typedSession *SessionCache
|
||||||
|
mu sync.RWMutex
|
||||||
logger Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
+23
-42
@@ -48,23 +48,12 @@ func (s State) String() string {
|
|||||||
|
|
||||||
// CircuitBreakerConfig holds configuration for the circuit breaker
|
// CircuitBreakerConfig holds configuration for the circuit breaker
|
||||||
type CircuitBreakerConfig struct {
|
type CircuitBreakerConfig struct {
|
||||||
// MaxFailures is the number of consecutive failures before opening the circuit
|
OnStateChange func(from, to State)
|
||||||
MaxFailures int
|
MaxFailures int
|
||||||
|
FailureThreshold float64
|
||||||
// FailureThreshold is the failure rate threshold (0.0 to 1.0)
|
Timeout time.Duration
|
||||||
FailureThreshold float64
|
|
||||||
|
|
||||||
// Timeout is how long the circuit stays open before trying half-open
|
|
||||||
Timeout time.Duration
|
|
||||||
|
|
||||||
// HalfOpenMaxRequests is the number of requests allowed in half-open state
|
|
||||||
HalfOpenMaxRequests int
|
HalfOpenMaxRequests int
|
||||||
|
ResetTimeout time.Duration
|
||||||
// ResetTimeout is how long to wait before resetting counters in closed state
|
|
||||||
ResetTimeout time.Duration
|
|
||||||
|
|
||||||
// OnStateChange is called when the circuit breaker changes state
|
|
||||||
OnStateChange func(from, to State)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultCircuitBreakerConfig returns default configuration
|
// DefaultCircuitBreakerConfig returns default configuration
|
||||||
@@ -80,28 +69,20 @@ func DefaultCircuitBreakerConfig() *CircuitBreakerConfig {
|
|||||||
|
|
||||||
// CircuitBreaker implements the circuit breaker pattern
|
// CircuitBreaker implements the circuit breaker pattern
|
||||||
type CircuitBreaker struct {
|
type CircuitBreaker struct {
|
||||||
config *CircuitBreakerConfig
|
nextRetryTime time.Time
|
||||||
|
lastStateChange time.Time
|
||||||
// State management
|
lastSuccessTime time.Time
|
||||||
state atomic.Int32
|
lastFailureTime time.Time
|
||||||
lastStateChange time.Time
|
config *CircuitBreakerConfig
|
||||||
stateMu sync.RWMutex
|
|
||||||
|
|
||||||
// Failure tracking
|
|
||||||
consecutiveFailures atomic.Int32
|
|
||||||
totalRequests atomic.Int64
|
|
||||||
totalFailures atomic.Int64
|
totalFailures atomic.Int64
|
||||||
|
totalRequests atomic.Int64
|
||||||
|
stateTransitions atomic.Int64
|
||||||
|
rejectedRequests atomic.Int64
|
||||||
|
stateMu sync.RWMutex
|
||||||
|
timeMu sync.RWMutex
|
||||||
halfOpenRequests atomic.Int32
|
halfOpenRequests atomic.Int32
|
||||||
|
consecutiveFailures atomic.Int32
|
||||||
// Timing
|
state atomic.Int32
|
||||||
lastFailureTime time.Time
|
|
||||||
lastSuccessTime time.Time
|
|
||||||
nextRetryTime time.Time
|
|
||||||
timeMu sync.RWMutex
|
|
||||||
|
|
||||||
// Metrics
|
|
||||||
stateTransitions atomic.Int64
|
|
||||||
rejectedRequests atomic.Int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCircuitBreaker creates a new circuit breaker
|
// NewCircuitBreaker creates a new circuit breaker
|
||||||
@@ -313,17 +294,17 @@ func (cb *CircuitBreaker) Stats() CircuitBreakerStats {
|
|||||||
|
|
||||||
// CircuitBreakerStats holds statistics for the circuit breaker
|
// CircuitBreakerStats holds statistics for the circuit breaker
|
||||||
type CircuitBreakerStats struct {
|
type CircuitBreakerStats struct {
|
||||||
State State
|
LastFailureTime time.Time
|
||||||
ConsecutiveFailures int32
|
LastSuccessTime time.Time
|
||||||
|
LastStateChange time.Time
|
||||||
|
NextRetryTime time.Time
|
||||||
TotalRequests int64
|
TotalRequests int64
|
||||||
TotalFailures int64
|
TotalFailures int64
|
||||||
SuccessRate float64
|
SuccessRate float64
|
||||||
RejectedRequests int64
|
RejectedRequests int64
|
||||||
StateTransitions int64
|
StateTransitions int64
|
||||||
LastFailureTime time.Time
|
State State
|
||||||
LastSuccessTime time.Time
|
ConsecutiveFailures int32
|
||||||
LastStateChange time.Time
|
|
||||||
NextRetryTime time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsHealthy returns true if the circuit breaker is in a healthy state
|
// IsHealthy returns true if the circuit breaker is in a healthy state
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ type mockBackend struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mockEntry struct {
|
type mockEntry struct {
|
||||||
value []byte
|
|
||||||
expiresAt time.Time
|
expiresAt time.Time
|
||||||
|
value []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockBackend() *mockBackend {
|
func newMockBackend() *mockBackend {
|
||||||
|
|||||||
+28
-49
@@ -41,26 +41,13 @@ func (h HealthStatus) String() string {
|
|||||||
|
|
||||||
// HealthCheckConfig holds configuration for the health checker
|
// HealthCheckConfig holds configuration for the health checker
|
||||||
type HealthCheckConfig struct {
|
type HealthCheckConfig struct {
|
||||||
// CheckInterval is how often to check health
|
OnStatusChange func(from, to HealthStatus)
|
||||||
CheckInterval time.Duration
|
CheckFunc func(ctx context.Context) error
|
||||||
|
CheckInterval time.Duration
|
||||||
// Timeout is the timeout for each health check
|
Timeout time.Duration
|
||||||
Timeout time.Duration
|
HealthyThreshold int
|
||||||
|
|
||||||
// HealthyThreshold is the number of consecutive successes to become healthy
|
|
||||||
HealthyThreshold int
|
|
||||||
|
|
||||||
// UnhealthyThreshold is the number of consecutive failures to become unhealthy
|
|
||||||
UnhealthyThreshold int
|
UnhealthyThreshold int
|
||||||
|
DegradedThreshold time.Duration
|
||||||
// DegradedThreshold is the latency threshold in ms to mark as degraded
|
|
||||||
DegradedThreshold time.Duration
|
|
||||||
|
|
||||||
// OnStatusChange is called when health status changes
|
|
||||||
OnStatusChange func(from, to HealthStatus)
|
|
||||||
|
|
||||||
// CheckFunc is the function to check health
|
|
||||||
CheckFunc func(ctx context.Context) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultHealthCheckConfig returns default configuration
|
// DefaultHealthCheckConfig returns default configuration
|
||||||
@@ -76,31 +63,23 @@ func DefaultHealthCheckConfig() *HealthCheckConfig {
|
|||||||
|
|
||||||
// HealthChecker monitors the health of a backend
|
// HealthChecker monitors the health of a backend
|
||||||
type HealthChecker struct {
|
type HealthChecker struct {
|
||||||
config *HealthCheckConfig
|
lastCheckTime time.Time
|
||||||
|
lastSuccessTime time.Time
|
||||||
// Status tracking
|
lastFailureTime time.Time
|
||||||
status atomic.Int32
|
config *HealthCheckConfig
|
||||||
consecutiveSuccesses atomic.Int32
|
stopChan chan struct{}
|
||||||
|
ticker *time.Ticker
|
||||||
|
wg sync.WaitGroup
|
||||||
|
statusChanges atomic.Int64
|
||||||
|
totalChecks atomic.Int64
|
||||||
|
totalSuccesses atomic.Int64
|
||||||
|
totalFailures atomic.Int64
|
||||||
|
averageLatency atomic.Int64
|
||||||
|
timeMu sync.RWMutex
|
||||||
consecutiveFailures atomic.Int32
|
consecutiveFailures atomic.Int32
|
||||||
|
consecutiveSuccesses atomic.Int32
|
||||||
// Timing
|
stopped atomic.Bool
|
||||||
lastCheckTime time.Time
|
status atomic.Int32
|
||||||
lastSuccessTime time.Time
|
|
||||||
lastFailureTime time.Time
|
|
||||||
averageLatency atomic.Int64
|
|
||||||
timeMu sync.RWMutex
|
|
||||||
|
|
||||||
// Metrics
|
|
||||||
totalChecks atomic.Int64
|
|
||||||
totalSuccesses atomic.Int64
|
|
||||||
totalFailures atomic.Int64
|
|
||||||
statusChanges atomic.Int64
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
ticker *time.Ticker
|
|
||||||
stopChan chan struct{}
|
|
||||||
stopped atomic.Bool
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHealthChecker creates a new health checker
|
// NewHealthChecker creates a new health checker
|
||||||
@@ -342,19 +321,19 @@ func (hc *HealthChecker) Stats() HealthCheckerStats {
|
|||||||
|
|
||||||
// HealthCheckerStats holds statistics for the health checker
|
// HealthCheckerStats holds statistics for the health checker
|
||||||
type HealthCheckerStats struct {
|
type HealthCheckerStats struct {
|
||||||
Status HealthStatus
|
LastCheckTime time.Time
|
||||||
ConsecutiveSuccesses int32
|
LastFailureTime time.Time
|
||||||
ConsecutiveFailures int32
|
LastSuccessTime time.Time
|
||||||
TotalChecks int64
|
TotalChecks int64
|
||||||
TotalSuccesses int64
|
TotalSuccesses int64
|
||||||
TotalFailures int64
|
TotalFailures int64
|
||||||
SuccessRate float64
|
SuccessRate float64
|
||||||
AverageLatency time.Duration
|
AverageLatency time.Duration
|
||||||
StatusChanges int64
|
StatusChanges int64
|
||||||
LastCheckTime time.Time
|
|
||||||
LastSuccessTime time.Time
|
|
||||||
LastFailureTime time.Time
|
|
||||||
HealthScore float64
|
HealthScore float64
|
||||||
|
Status HealthStatus
|
||||||
|
ConsecutiveFailures int32
|
||||||
|
ConsecutiveSuccesses int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset resets the health checker statistics
|
// Reset resets the health checker statistics
|
||||||
|
|||||||
+7
-11
@@ -12,20 +12,16 @@ import (
|
|||||||
|
|
||||||
// HealthCheckBackend wraps a cache backend with health checking
|
// HealthCheckBackend wraps a cache backend with health checking
|
||||||
type HealthCheckBackend struct {
|
type HealthCheckBackend struct {
|
||||||
backend backends.CacheBackend
|
lastCheck time.Time
|
||||||
config *HealthCheckConfig
|
backend backends.CacheBackend
|
||||||
|
ctx context.Context
|
||||||
// Health tracking
|
config *HealthCheckConfig
|
||||||
|
cancel context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
|
checkMutex sync.RWMutex
|
||||||
status atomic.Int32
|
status atomic.Int32
|
||||||
consecutiveFails atomic.Int32
|
consecutiveFails atomic.Int32
|
||||||
consecutiveOK atomic.Int32
|
consecutiveOK atomic.Int32
|
||||||
lastCheck time.Time
|
|
||||||
checkMutex sync.RWMutex
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHealthCheckBackend creates a new health check wrapped backend
|
// NewHealthCheckBackend creates a new health check wrapped backend
|
||||||
|
|||||||
Vendored
+2
-2
@@ -292,12 +292,12 @@ type SessionCache struct {
|
|||||||
|
|
||||||
// SessionData represents session information
|
// SessionData represents session information
|
||||||
type SessionData struct {
|
type SessionData struct {
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
Claims map[string]interface{} `json:"claims"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
|
||||||
Claims map[string]interface{} `json:"claims"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSessionCache creates a new session cache
|
// NewSessionCache creates a new session cache
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
|
|
||||||
// Mock logger for testing
|
// Mock logger for testing
|
||||||
type mockLogger struct {
|
type mockLogger struct {
|
||||||
mu sync.Mutex
|
|
||||||
logs []string
|
logs []string
|
||||||
errLogs []string
|
errLogs []string
|
||||||
debugLog []string
|
debugLog []string
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockLogger) Logf(format string, args ...interface{}) {
|
func (m *mockLogger) Logf(format string, args ...interface{}) {
|
||||||
|
|||||||
+11
-11
@@ -19,20 +19,20 @@ type Logger interface {
|
|||||||
|
|
||||||
// BackgroundTask represents a recurring background task
|
// BackgroundTask represents a recurring background task
|
||||||
type BackgroundTask struct {
|
type BackgroundTask struct {
|
||||||
name string
|
lastRun time.Time
|
||||||
interval time.Duration
|
logger Logger
|
||||||
taskFunc func()
|
ctx context.Context
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
stopChan chan bool
|
stopChan chan bool
|
||||||
isRunning int32
|
|
||||||
logger Logger
|
|
||||||
waitGroup *sync.WaitGroup
|
waitGroup *sync.WaitGroup
|
||||||
lastRun time.Time
|
taskFunc func()
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
name string
|
||||||
runCount int64
|
runCount int64
|
||||||
errorCount int64
|
errorCount int64
|
||||||
|
interval time.Duration
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
isRunning int32
|
||||||
cancelFunc context.CancelFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBackgroundTask creates a new background task
|
// NewBackgroundTask creates a new background task
|
||||||
@@ -183,11 +183,11 @@ func (bt *BackgroundTask) IsRunning() bool {
|
|||||||
|
|
||||||
// TaskRegistry manages all background tasks
|
// TaskRegistry manages all background tasks
|
||||||
type TaskRegistry struct {
|
type TaskRegistry struct {
|
||||||
tasks map[string]*BackgroundTask
|
|
||||||
mu sync.RWMutex
|
|
||||||
logger Logger
|
logger Logger
|
||||||
maxTasks int
|
tasks map[string]*BackgroundTask
|
||||||
circuitBreaker *TaskCircuitBreaker
|
circuitBreaker *TaskCircuitBreaker
|
||||||
|
maxTasks int
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// globalTaskRegistry is the singleton task registry
|
// globalTaskRegistry is the singleton task registry
|
||||||
|
|||||||
+13
-13
@@ -11,14 +11,14 @@ import (
|
|||||||
|
|
||||||
// TaskCircuitBreaker prevents task creation failures from cascading
|
// TaskCircuitBreaker prevents task creation failures from cascading
|
||||||
type TaskCircuitBreaker struct {
|
type TaskCircuitBreaker struct {
|
||||||
|
lastFailureTime time.Time
|
||||||
|
logger Logger
|
||||||
|
taskFailures map[string]int32
|
||||||
|
timeout time.Duration
|
||||||
|
mu sync.RWMutex
|
||||||
failureThreshold int32
|
failureThreshold int32
|
||||||
failureCount int32
|
failureCount int32
|
||||||
lastFailureTime time.Time
|
state int32
|
||||||
timeout time.Duration
|
|
||||||
state int32 // 0: closed, 1: open
|
|
||||||
logger Logger
|
|
||||||
mu sync.RWMutex
|
|
||||||
taskFailures map[string]int32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CircuitBreakerState represents the state of the circuit breaker
|
// CircuitBreakerState represents the state of the circuit breaker
|
||||||
@@ -140,14 +140,14 @@ func (cb *TaskCircuitBreaker) GetState() CircuitBreakerState {
|
|||||||
|
|
||||||
// TaskMemoryMonitor monitors memory usage and can trigger cleanup
|
// TaskMemoryMonitor monitors memory usage and can trigger cleanup
|
||||||
type TaskMemoryMonitor struct {
|
type TaskMemoryMonitor struct {
|
||||||
|
lastCheck time.Time
|
||||||
logger Logger
|
logger Logger
|
||||||
registry *TaskRegistry
|
registry *TaskRegistry
|
||||||
|
stopChan chan bool
|
||||||
memoryThreshold uint64
|
memoryThreshold uint64
|
||||||
checkInterval time.Duration
|
checkInterval time.Duration
|
||||||
isMonitoring int32
|
|
||||||
stopChan chan bool
|
|
||||||
lastCheck time.Time
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
isMonitoring int32
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -310,13 +310,13 @@ func (tmm *TaskMemoryMonitor) GetStats() map[string]interface{} {
|
|||||||
|
|
||||||
// WorkerPool manages a pool of worker goroutines for task execution
|
// WorkerPool manages a pool of worker goroutines for task execution
|
||||||
type WorkerPool struct {
|
type WorkerPool struct {
|
||||||
workers int
|
|
||||||
taskQueue chan func()
|
|
||||||
workerWg sync.WaitGroup
|
|
||||||
isRunning int32
|
|
||||||
logger Logger
|
logger Logger
|
||||||
|
taskQueue chan func()
|
||||||
stopChan chan bool
|
stopChan chan bool
|
||||||
metrics WorkerPoolMetrics
|
metrics WorkerPoolMetrics
|
||||||
|
workerWg sync.WaitGroup
|
||||||
|
workers int
|
||||||
|
isRunning int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkerPoolMetrics tracks worker pool performance
|
// WorkerPoolMetrics tracks worker pool performance
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
type FeatureFlag struct {
|
type FeatureFlag struct {
|
||||||
name string
|
name string
|
||||||
description string
|
description string
|
||||||
enabled atomic.Bool
|
|
||||||
mu sync.RWMutex
|
|
||||||
callbacks []func(bool)
|
callbacks []func(bool)
|
||||||
|
mu sync.RWMutex
|
||||||
|
enabled atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// FeatureManager manages all feature flags in the application
|
// FeatureManager manages all feature flags in the application
|
||||||
|
|||||||
+19
-28
@@ -14,50 +14,41 @@ import (
|
|||||||
// and resource leaks. It provides centralized management of HTTP client transports with
|
// and resource leaks. It provides centralized management of HTTP client transports with
|
||||||
// proper lifecycle management and security controls.
|
// proper lifecycle management and security controls.
|
||||||
type TransportPool struct {
|
type TransportPool struct {
|
||||||
mu sync.RWMutex
|
|
||||||
transports map[string]*sharedTransport
|
|
||||||
maxConns int
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
transports map[string]*sharedTransport
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
clientCount int32 // Track total HTTP clients
|
maxConns int
|
||||||
maxClients int32 // Limit total clients
|
mu sync.RWMutex
|
||||||
|
clientCount int32
|
||||||
|
maxClients int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedTransport wraps an HTTP transport with reference counting
|
// sharedTransport wraps an HTTP transport with reference counting
|
||||||
type sharedTransport struct {
|
type sharedTransport struct {
|
||||||
transport *http.Transport
|
|
||||||
refCount int32
|
|
||||||
lastUsed time.Time
|
lastUsed time.Time
|
||||||
|
transport *http.Transport
|
||||||
config TransportConfig
|
config TransportConfig
|
||||||
|
refCount int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransportConfig defines configuration for HTTP transports
|
// TransportConfig defines configuration for HTTP transports
|
||||||
type TransportConfig struct {
|
type TransportConfig struct {
|
||||||
// Timeouts
|
MaxConnsPerHost int
|
||||||
DialTimeout time.Duration
|
WriteBufferSize int
|
||||||
TLSHandshakeTimeout time.Duration
|
|
||||||
ResponseHeaderTimeout time.Duration
|
ResponseHeaderTimeout time.Duration
|
||||||
ExpectContinueTimeout time.Duration
|
ExpectContinueTimeout time.Duration
|
||||||
IdleConnTimeout time.Duration
|
IdleConnTimeout time.Duration
|
||||||
KeepAlive time.Duration
|
KeepAlive time.Duration
|
||||||
|
TLSHandshakeTimeout time.Duration
|
||||||
// Connection limits
|
MaxIdleConns int
|
||||||
MaxIdleConns int
|
DialTimeout time.Duration
|
||||||
MaxIdleConnsPerHost int
|
MaxIdleConnsPerHost int
|
||||||
MaxConnsPerHost int
|
ReadBufferSize int
|
||||||
|
MinTLSVersion uint16
|
||||||
// Features
|
ForceHTTP2 bool
|
||||||
ForceHTTP2 bool
|
DisableCompression bool
|
||||||
DisableKeepAlives bool
|
InsecureSkipVerify bool
|
||||||
DisableCompression bool
|
DisableKeepAlives bool
|
||||||
|
|
||||||
// Buffer sizes
|
|
||||||
WriteBufferSize int
|
|
||||||
ReadBufferSize int
|
|
||||||
|
|
||||||
// TLS
|
|
||||||
InsecureSkipVerify bool
|
|
||||||
MinTLSVersion uint16
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -154,10 +154,10 @@ func TestAzureProvider_ValidateTokens(t *testing.T) {
|
|||||||
provider := NewAzureProvider()
|
provider := NewAzureProvider()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
session *mockSession
|
|
||||||
verifierError error
|
verifierError error
|
||||||
|
session *mockSession
|
||||||
cacheData map[string]interface{}
|
cacheData map[string]interface{}
|
||||||
|
name string
|
||||||
expectedResult ValidationResult
|
expectedResult ValidationResult
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -369,9 +369,9 @@ func TestAzureProvider_OfflineAccessHandling(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
inputScopes []string
|
|
||||||
expectedCount int // Expected number of offline_access scopes (should be 1)
|
|
||||||
description string
|
description string
|
||||||
|
inputScopes []string
|
||||||
|
expectedCount int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "No offline_access - should add one",
|
name: "No offline_access - should add one",
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
|
|
||||||
// Mock implementations for testing
|
// Mock implementations for testing
|
||||||
type mockSession struct {
|
type mockSession struct {
|
||||||
authenticated bool
|
|
||||||
idToken string
|
idToken string
|
||||||
accessToken string
|
accessToken string
|
||||||
refreshToken string
|
refreshToken string
|
||||||
|
authenticated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mockSession) GetIDToken() string { return s.idToken }
|
func (s *mockSession) GetIDToken() string { return s.idToken }
|
||||||
@@ -338,10 +338,10 @@ func TestBaseProvider_ValidateTokenExpiry(t *testing.T) {
|
|||||||
gracePeriod := 5 * time.Minute
|
gracePeriod := 5 * time.Minute
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
claims map[string]interface{}
|
claims map[string]interface{}
|
||||||
cacheFound bool
|
name string
|
||||||
expectedResult ValidationResult
|
expectedResult ValidationResult
|
||||||
|
cacheFound bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Token not found in cache, has refresh token",
|
name: "Token not found in cache, has refresh token",
|
||||||
@@ -438,10 +438,10 @@ func TestBaseProvider_ValidateTokenExpiry_NoRefreshToken(t *testing.T) {
|
|||||||
gracePeriod := 5 * time.Minute
|
gracePeriod := 5 * time.Minute
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
claims map[string]interface{}
|
claims map[string]interface{}
|
||||||
cacheFound bool
|
name string
|
||||||
expectedResult ValidationResult
|
expectedResult ValidationResult
|
||||||
|
cacheFound bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Token not found in cache, no refresh token",
|
name: "Token not found in cache, no refresh token",
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ func TestProviderFactory_CreateProvider(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
issuerURL string
|
issuerURL string
|
||||||
|
errMsg string
|
||||||
expectedType ProviderType
|
expectedType ProviderType
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errMsg string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Google provider",
|
name: "Google provider",
|
||||||
@@ -158,10 +158,10 @@ func TestProviderFactory_CreateProviderByType(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
errMsg string
|
||||||
providerType ProviderType
|
providerType ProviderType
|
||||||
expectedType ProviderType
|
expectedType ProviderType
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errMsg string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Generic provider",
|
name: "Generic provider",
|
||||||
|
|||||||
@@ -136,9 +136,9 @@ func TestGenericProvider_ValidateTokens(t *testing.T) {
|
|||||||
provider := NewGenericProvider()
|
provider := NewGenericProvider()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
session *mockSession
|
|
||||||
verifierError error
|
verifierError error
|
||||||
|
session *mockSession
|
||||||
|
name string
|
||||||
expectedResult ValidationResult
|
expectedResult ValidationResult
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ func TestGoogleProvider_OfflineAccessFiltering(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
inputScopes []string
|
|
||||||
description string
|
description string
|
||||||
|
inputScopes []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Multiple offline_access occurrences",
|
name: "Multiple offline_access occurrences",
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ func TestProviderRegistry_GetProviderByType(t *testing.T) {
|
|||||||
registry.RegisterProvider(googleProvider)
|
registry.RegisterProvider(googleProvider)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
expected OIDCProvider
|
||||||
name string
|
name string
|
||||||
providerType ProviderType
|
providerType ProviderType
|
||||||
expected OIDCProvider
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Get Generic provider",
|
name: "Get Generic provider",
|
||||||
@@ -180,9 +180,9 @@ func TestProviderRegistry_DetectProvider(t *testing.T) {
|
|||||||
registry.RegisterProvider(gitlabProvider)
|
registry.RegisterProvider(gitlabProvider)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
expected OIDCProvider
|
||||||
name string
|
name string
|
||||||
issuerURL string
|
issuerURL string
|
||||||
expected OIDCProvider
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Google provider detection",
|
name: "Google provider detection",
|
||||||
@@ -640,9 +640,9 @@ func TestProviderRegistry_GitLabDetection_RealWorldURLs(t *testing.T) {
|
|||||||
registry.RegisterProvider(githubProvider)
|
registry.RegisterProvider(githubProvider)
|
||||||
|
|
||||||
realWorldTests := []struct {
|
realWorldTests := []struct {
|
||||||
|
expected OIDCProvider
|
||||||
name string
|
name string
|
||||||
issuerURL string
|
issuerURL string
|
||||||
expected OIDCProvider
|
|
||||||
}{
|
}{
|
||||||
// Actual self-hosted GitLab examples from issue #61
|
// Actual self-hosted GitLab examples from issue #61
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ func TestValidateIssuerURL(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
issuerURL string
|
issuerURL string
|
||||||
wantErr bool
|
|
||||||
errMsg string
|
errMsg string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid https URL",
|
name: "valid https URL",
|
||||||
@@ -106,8 +106,8 @@ func TestValidateClientID(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
clientID string
|
clientID string
|
||||||
wantErr bool
|
|
||||||
errMsg string
|
errMsg string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid client ID",
|
name: "valid client ID",
|
||||||
@@ -173,9 +173,9 @@ func TestValidateClientID(t *testing.T) {
|
|||||||
func TestValidateScopes(t *testing.T) {
|
func TestValidateScopes(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
errMsg string
|
||||||
scopes []string
|
scopes []string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errMsg string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid scopes with openid",
|
name: "valid scopes with openid",
|
||||||
@@ -248,8 +248,8 @@ func TestValidateRedirectURL(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
redirectURL string
|
redirectURL string
|
||||||
wantErr bool
|
|
||||||
errMsg string
|
errMsg string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid https redirect URL",
|
name: "valid https redirect URL",
|
||||||
@@ -315,11 +315,11 @@ func TestValidateRedirectURL(t *testing.T) {
|
|||||||
// TestValidateProviderSpecificConfig tests provider-specific configuration validation
|
// TestValidateProviderSpecificConfig tests provider-specific configuration validation
|
||||||
func TestValidateProviderSpecificConfig(t *testing.T) {
|
func TestValidateProviderSpecificConfig(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
provider OIDCProvider
|
provider OIDCProvider
|
||||||
config map[string]interface{}
|
config map[string]interface{}
|
||||||
wantErr bool
|
name string
|
||||||
errMsg string
|
errMsg string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid Google config",
|
name: "valid Google config",
|
||||||
@@ -458,8 +458,8 @@ func TestValidateGoogleConfig_EdgeCases(t *testing.T) {
|
|||||||
googleProvider := NewGoogleProvider()
|
googleProvider := NewGoogleProvider()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
config map[string]interface{}
|
config map[string]interface{}
|
||||||
|
name string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -502,10 +502,10 @@ func TestValidateAzureConfig_EdgeCases(t *testing.T) {
|
|||||||
azureProvider := NewAzureProvider()
|
azureProvider := NewAzureProvider()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
config map[string]interface{}
|
config map[string]interface{}
|
||||||
wantErr bool
|
name string
|
||||||
errMsg string
|
errMsg string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid tenant ID format",
|
name: "valid tenant ID format",
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
|
|
||||||
// ProviderWarning represents a warning about provider limitations or requirements.
|
// ProviderWarning represents a warning about provider limitations or requirements.
|
||||||
type ProviderWarning struct {
|
type ProviderWarning struct {
|
||||||
ProviderType ProviderType
|
Level string
|
||||||
Level string // "info", "warning", "error"
|
|
||||||
Message string
|
Message string
|
||||||
|
ProviderType ProviderType
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProviderWarnings returns warnings about provider-specific limitations.
|
// GetProviderWarnings returns warnings about provider-specific limitations.
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
func TestGetProviderWarnings(t *testing.T) {
|
func TestGetProviderWarnings(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
checkContent string
|
||||||
providerType ProviderType
|
providerType ProviderType
|
||||||
expectCount int
|
expectCount int
|
||||||
checkContent string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "GitHub has OAuth 2.0 warning",
|
name: "GitHub has OAuth 2.0 warning",
|
||||||
|
|||||||
@@ -34,21 +34,15 @@ type Logger interface {
|
|||||||
// for all recovery mechanism implementations. It handles request counting,
|
// for all recovery mechanism implementations. It handles request counting,
|
||||||
// success/failure tracking, and timestamp management in a thread-safe manner.
|
// success/failure tracking, and timestamp management in a thread-safe manner.
|
||||||
type BaseRecoveryMechanism struct {
|
type BaseRecoveryMechanism struct {
|
||||||
// name identifies the recovery mechanism instance
|
logger Logger
|
||||||
name string
|
name string
|
||||||
// logger provides structured logging capabilities
|
lastSuccessStr string
|
||||||
logger Logger
|
lastFailureStr string
|
||||||
|
|
||||||
// Metrics tracked with atomic operations for thread safety
|
|
||||||
totalRequests int64
|
totalRequests int64
|
||||||
successCount int64
|
successCount int64
|
||||||
failureCount int64
|
failureCount int64
|
||||||
lastSuccessStr string
|
successMutex sync.RWMutex
|
||||||
lastFailureStr string
|
failureMutex sync.RWMutex
|
||||||
|
|
||||||
// mutexes for thread-safe timestamp updates
|
|
||||||
successMutex sync.RWMutex
|
|
||||||
failureMutex sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBaseRecoveryMechanism creates a new base recovery mechanism with the given name and logger.
|
// NewBaseRecoveryMechanism creates a new base recovery mechanism with the given name and logger.
|
||||||
@@ -182,10 +176,10 @@ const (
|
|||||||
|
|
||||||
// HTTPError represents an HTTP error with status code and message
|
// HTTPError represents an HTTP error with status code and message
|
||||||
type HTTPError struct {
|
type HTTPError struct {
|
||||||
StatusCode int
|
Headers map[string]string
|
||||||
Message string
|
Message string
|
||||||
Body []byte
|
Body []byte
|
||||||
Headers map[string]string
|
StatusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error implements the error interface
|
// Error implements the error interface
|
||||||
|
|||||||
@@ -60,20 +60,14 @@ func DefaultCircuitBreakerConfig() CircuitBreakerConfig {
|
|||||||
// CircuitBreaker implements the circuit breaker pattern for fault tolerance.
|
// CircuitBreaker implements the circuit breaker pattern for fault tolerance.
|
||||||
// It prevents cascading failures by temporarily blocking requests to a failing service.
|
// It prevents cascading failures by temporarily blocking requests to a failing service.
|
||||||
type CircuitBreaker struct {
|
type CircuitBreaker struct {
|
||||||
*BaseRecoveryMechanism
|
|
||||||
config CircuitBreakerConfig
|
|
||||||
|
|
||||||
// State management
|
|
||||||
state int32 // atomic: CircuitBreakerState
|
|
||||||
lastStateChange time.Time
|
lastStateChange time.Time
|
||||||
stateMutex sync.RWMutex
|
*BaseRecoveryMechanism
|
||||||
|
config CircuitBreakerConfig
|
||||||
// Failure tracking
|
stateMutex sync.RWMutex
|
||||||
consecutiveFailures int32 // atomic
|
state int32
|
||||||
consecutiveSuccesses int32 // atomic
|
consecutiveFailures int32
|
||||||
|
consecutiveSuccesses int32
|
||||||
// Half-open state management
|
halfOpenRequests int32
|
||||||
halfOpenRequests int32 // atomic
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCircuitBreaker creates a new circuit breaker with the given configuration
|
// NewCircuitBreaker creates a new circuit breaker with the given configuration
|
||||||
|
|||||||
@@ -15,20 +15,13 @@ import (
|
|||||||
|
|
||||||
// RetryConfig defines configuration for the retry executor
|
// RetryConfig defines configuration for the retry executor
|
||||||
type RetryConfig struct {
|
type RetryConfig struct {
|
||||||
// MaxAttempts is the maximum number of retry attempts
|
RetryableErrors []string
|
||||||
MaxAttempts int
|
|
||||||
// InitialDelay is the initial delay between retries
|
|
||||||
InitialDelay time.Duration
|
|
||||||
// MaxDelay is the maximum delay between retries
|
|
||||||
MaxDelay time.Duration
|
|
||||||
// Multiplier is the backoff multiplier
|
|
||||||
Multiplier float64
|
|
||||||
// RandomizationFactor adds jitter to delays (0.0 to 1.0)
|
|
||||||
RandomizationFactor float64
|
|
||||||
// RetryableErrors defines which errors should trigger a retry
|
|
||||||
RetryableErrors []string
|
|
||||||
// RetryableStatusCodes defines which HTTP status codes should trigger a retry
|
|
||||||
RetryableStatusCodes []int
|
RetryableStatusCodes []int
|
||||||
|
MaxAttempts int
|
||||||
|
InitialDelay time.Duration
|
||||||
|
MaxDelay time.Duration
|
||||||
|
Multiplier float64
|
||||||
|
RandomizationFactor float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultRetryConfig returns sensible default retry configuration
|
// DefaultRetryConfig returns sensible default retry configuration
|
||||||
@@ -46,13 +39,11 @@ func DefaultRetryConfig() RetryConfig {
|
|||||||
|
|
||||||
// RetryExecutor implements retry logic with exponential backoff
|
// RetryExecutor implements retry logic with exponential backoff
|
||||||
type RetryExecutor struct {
|
type RetryExecutor struct {
|
||||||
|
lastRetryTime time.Time
|
||||||
*BaseRecoveryMechanism
|
*BaseRecoveryMechanism
|
||||||
config RetryConfig
|
config RetryConfig
|
||||||
|
|
||||||
// Metrics
|
|
||||||
totalRetries int64
|
totalRetries int64
|
||||||
maxRetriesHit int64
|
maxRetriesHit int64
|
||||||
lastRetryTime time.Time
|
|
||||||
retryTimeMutex sync.RWMutex
|
retryTimeMutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,17 +273,17 @@ func TestRetryExecutor_isRetryableError(t *testing.T) {
|
|||||||
executor := NewRetryExecutor(config, logger)
|
executor := NewRetryExecutor(config, logger)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
err error
|
err error
|
||||||
|
name string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{"nil error", nil, false},
|
{name: "nil error", err: nil, expected: false},
|
||||||
{"connection refused", errors.New("connection refused"), true},
|
{name: "connection refused", err: errors.New("connection refused"), expected: true},
|
||||||
{"timeout", errors.New("TIMEOUT"), true}, // case insensitive
|
{name: "timeout", err: errors.New("TIMEOUT"), expected: true}, // case insensitive
|
||||||
{"EOF", errors.New("EOF"), false},
|
{name: "EOF", err: errors.New("EOF"), expected: false},
|
||||||
{"random error", errors.New("something else"), false},
|
{name: "random error", err: errors.New("something else"), expected: false},
|
||||||
{"context cancelled", context.Canceled, false},
|
{name: "context cancelled", err: context.Canceled, expected: false},
|
||||||
{"context deadline exceeded", context.DeadlineExceeded, false},
|
{name: "context deadline exceeded", err: context.DeadlineExceeded, expected: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
|
|
||||||
// Mock logger for testing
|
// Mock logger for testing
|
||||||
type mockLogger struct {
|
type mockLogger struct {
|
||||||
mu sync.Mutex
|
|
||||||
logs []string
|
logs []string
|
||||||
errLogs []string
|
errLogs []string
|
||||||
debugLog []string
|
debugLog []string
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockLogger) Logf(format string, args ...interface{}) {
|
func (m *mockLogger) Logf(format string, args ...interface{}) {
|
||||||
@@ -202,13 +202,13 @@ func TestBaseRecoveryMechanism_ConcurrentAccess(t *testing.T) {
|
|||||||
// CircuitBreakerState tests
|
// CircuitBreakerState tests
|
||||||
func TestCircuitBreakerState_String(t *testing.T) {
|
func TestCircuitBreakerState_String(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
state CircuitBreakerState
|
|
||||||
expected string
|
expected string
|
||||||
|
state CircuitBreakerState
|
||||||
}{
|
}{
|
||||||
{CircuitBreakerClosed, "closed"},
|
{state: CircuitBreakerClosed, expected: "closed"},
|
||||||
{CircuitBreakerOpen, "open"},
|
{state: CircuitBreakerOpen, expected: "open"},
|
||||||
{CircuitBreakerHalfOpen, "half-open"},
|
{state: CircuitBreakerHalfOpen, expected: "half-open"},
|
||||||
{CircuitBreakerState(99), "unknown"},
|
{state: CircuitBreakerState(99), expected: "unknown"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -29,26 +29,26 @@ type JWK struct {
|
|||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
RefreshToken string `json:"refresh_token,omitempty"`
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
IDToken string `json:"id_token,omitempty"`
|
IDToken string `json:"id_token,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntrospectionResponse represents a token introspection response
|
// IntrospectionResponse represents a token introspection response
|
||||||
type IntrospectionResponse struct {
|
type IntrospectionResponse struct {
|
||||||
Active bool `json:"active"`
|
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
TokenType string `json:"token_type,omitempty"`
|
TokenType string `json:"token_type,omitempty"`
|
||||||
Exp int64 `json:"exp,omitempty"`
|
|
||||||
Iat int64 `json:"iat,omitempty"`
|
|
||||||
Nbf int64 `json:"nbf,omitempty"`
|
|
||||||
Sub string `json:"sub,omitempty"`
|
Sub string `json:"sub,omitempty"`
|
||||||
Aud string `json:"aud,omitempty"`
|
Aud string `json:"aud,omitempty"`
|
||||||
Iss string `json:"iss,omitempty"`
|
Iss string `json:"iss,omitempty"`
|
||||||
Jti string `json:"jti,omitempty"`
|
Jti string `json:"jti,omitempty"`
|
||||||
|
Exp int64 `json:"exp,omitempty"`
|
||||||
|
Iat int64 `json:"iat,omitempty"`
|
||||||
|
Nbf int64 `json:"nbf,omitempty"`
|
||||||
|
Active bool `json:"active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWKCache is a testify mock for JWK caching operations
|
// JWKCache is a testify mock for JWK caching operations
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ import (
|
|||||||
|
|
||||||
// SessionData represents session data for testing
|
// SessionData represents session data for testing
|
||||||
type SessionData struct {
|
type SessionData struct {
|
||||||
|
Claims map[string]interface{}
|
||||||
Email string
|
Email string
|
||||||
AccessToken string
|
AccessToken string
|
||||||
RefreshToken string
|
RefreshToken string
|
||||||
IDToken string
|
IDToken string
|
||||||
Expiry int64
|
|
||||||
Nonce string
|
Nonce string
|
||||||
State string
|
State string
|
||||||
CodeVerifier string
|
CodeVerifier string
|
||||||
RedirectURL string
|
RedirectURL string
|
||||||
Claims map[string]interface{}
|
Expiry int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionManager is a testify mock for session management
|
// SessionManager is a testify mock for session management
|
||||||
|
|||||||
@@ -16,45 +16,30 @@ import (
|
|||||||
|
|
||||||
// OIDCServerConfig configures the mock OIDC server behavior
|
// OIDCServerConfig configures the mock OIDC server behavior
|
||||||
type OIDCServerConfig struct {
|
type OIDCServerConfig struct {
|
||||||
// Identity
|
JWKSResponse map[string]interface{}
|
||||||
Issuer string
|
TokenFixture *fixtures.TokenFixture
|
||||||
|
UserinfoError *OIDCError
|
||||||
// Discovery
|
UserinfoResponse map[string]interface{}
|
||||||
ScopesSupported []string
|
IntrospectionResponse map[string]interface{}
|
||||||
ResponseTypesSupported []string
|
JWKSError *OIDCError
|
||||||
|
RefreshError *OIDCError
|
||||||
|
TokenResponse map[string]interface{}
|
||||||
|
TokenError *OIDCError
|
||||||
|
IntrospectionError *OIDCError
|
||||||
|
RefreshResponse map[string]interface{}
|
||||||
|
Issuer string
|
||||||
GrantTypesSupported []string
|
GrantTypesSupported []string
|
||||||
ClaimsSupported []string
|
|
||||||
TokenEndpointAuthMethods []string
|
TokenEndpointAuthMethods []string
|
||||||
|
ScopesSupported []string
|
||||||
// Token fixture for signing
|
ClaimsSupported []string
|
||||||
TokenFixture *fixtures.TokenFixture
|
ResponseTypesSupported []string
|
||||||
|
FailAfterN int
|
||||||
// Token endpoint behavior
|
JWKSDelay time.Duration
|
||||||
TokenResponse map[string]interface{}
|
TimeoutDuration time.Duration
|
||||||
TokenError *OIDCError
|
RateLimitAfter int
|
||||||
TokenDelay time.Duration
|
TokenDelay time.Duration
|
||||||
RefreshResponse map[string]interface{}
|
FailWithStatus int
|
||||||
RefreshError *OIDCError
|
SimulateTimeout bool
|
||||||
|
|
||||||
// JWKS behavior
|
|
||||||
JWKSResponse map[string]interface{}
|
|
||||||
JWKSError *OIDCError
|
|
||||||
JWKSDelay time.Duration
|
|
||||||
|
|
||||||
// Introspection behavior
|
|
||||||
IntrospectionResponse map[string]interface{}
|
|
||||||
IntrospectionError *OIDCError
|
|
||||||
|
|
||||||
// Userinfo behavior
|
|
||||||
UserinfoResponse map[string]interface{}
|
|
||||||
UserinfoError *OIDCError
|
|
||||||
|
|
||||||
// Simulation flags
|
|
||||||
SimulateTimeout bool
|
|
||||||
TimeoutDuration time.Duration
|
|
||||||
RateLimitAfter int
|
|
||||||
FailAfterN int
|
|
||||||
FailWithStatus int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCError represents an OAuth error response
|
// OIDCError represents an OAuth error response
|
||||||
@@ -67,9 +52,9 @@ type OIDCError struct {
|
|||||||
type OIDCServer struct {
|
type OIDCServer struct {
|
||||||
*httptest.Server
|
*httptest.Server
|
||||||
Config *OIDCServerConfig
|
Config *OIDCServerConfig
|
||||||
RequestCount int32
|
|
||||||
mu sync.Mutex
|
|
||||||
requests []*http.Request
|
requests []*http.Request
|
||||||
|
mu sync.Mutex
|
||||||
|
RequestCount int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOIDCServer creates a new mock OIDC server
|
// NewOIDCServer creates a new mock OIDC server
|
||||||
|
|||||||
@@ -135,9 +135,9 @@ func TestIsTestMode(t *testing.T) {
|
|||||||
// We'll test what we can control via environment variables.
|
// We'll test what we can control via environment variables.
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
setup func()
|
setup func()
|
||||||
cleanup func()
|
cleanup func()
|
||||||
|
name string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -206,8 +206,8 @@ func TestIsTestMode(t *testing.T) {
|
|||||||
func TestIsTestModeEdgeCases(t *testing.T) {
|
func TestIsTestModeEdgeCases(t *testing.T) {
|
||||||
// Test with various environment variable combinations
|
// Test with various environment variable combinations
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
env map[string]string
|
env map[string]string
|
||||||
|
name string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "all env vars empty",
|
name: "all env vars empty",
|
||||||
@@ -560,11 +560,11 @@ func TestIsTestModeYaegiCompiler(t *testing.T) {
|
|||||||
|
|
||||||
// mockLogger is a simple mock implementation for testing
|
// mockLogger is a simple mock implementation for testing
|
||||||
type mockLogger struct {
|
type mockLogger struct {
|
||||||
|
lastFormat string
|
||||||
|
lastArgs []interface{}
|
||||||
infoCalls int
|
infoCalls int
|
||||||
debugCalls int
|
debugCalls int
|
||||||
errorCalls int
|
errorCalls int
|
||||||
lastFormat string
|
|
||||||
lastArgs []interface{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockLogger) Infof(format string, args ...interface{}) {
|
func (m *mockLogger) Infof(format string, args ...interface{}) {
|
||||||
|
|||||||
@@ -21,25 +21,16 @@ import (
|
|||||||
// JWK represents a JSON Web Key as defined in RFC 7517.
|
// JWK represents a JSON Web Key as defined in RFC 7517.
|
||||||
// It can represent different key types including RSA, EC, and symmetric keys.
|
// It can represent different key types including RSA, EC, and symmetric keys.
|
||||||
type JWK struct {
|
type JWK struct {
|
||||||
// Key type (e.g., "RSA", "EC", "oct")
|
Kty string `json:"kty"`
|
||||||
Kty string `json:"kty"`
|
Use string `json:"use,omitempty"`
|
||||||
// Key use (e.g., "sig" for signature, "enc" for encryption)
|
Alg string `json:"alg,omitempty"`
|
||||||
Use string `json:"use,omitempty"`
|
Kid string `json:"kid,omitempty"`
|
||||||
// Key operations allowed
|
N string `json:"n,omitempty"`
|
||||||
|
E string `json:"e,omitempty"`
|
||||||
|
Crv string `json:"crv,omitempty"`
|
||||||
|
X string `json:"x,omitempty"`
|
||||||
|
Y string `json:"y,omitempty"`
|
||||||
KeyOps []string `json:"key_ops,omitempty"`
|
KeyOps []string `json:"key_ops,omitempty"`
|
||||||
// Algorithm intended for use with this key
|
|
||||||
Alg string `json:"alg,omitempty"`
|
|
||||||
// Key ID
|
|
||||||
Kid string `json:"kid,omitempty"`
|
|
||||||
|
|
||||||
// RSA specific fields
|
|
||||||
N string `json:"n,omitempty"` // Modulus
|
|
||||||
E string `json:"e,omitempty"` // Exponent
|
|
||||||
|
|
||||||
// EC specific fields
|
|
||||||
Crv string `json:"crv,omitempty"` // Curve
|
|
||||||
X string `json:"x,omitempty"` // X coordinate
|
|
||||||
Y string `json:"y,omitempty"` // Y coordinate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWKSet represents a set of JSON Web Keys.
|
// JWKSet represents a set of JSON Web Keys.
|
||||||
|
|||||||
+9
-9
@@ -309,18 +309,18 @@ func TestJWKCacheCleanupAndClose(t *testing.T) {
|
|||||||
func TestFetchJWKSEdgeCases(t *testing.T) {
|
func TestFetchJWKSEdgeCases(t *testing.T) {
|
||||||
t.Run("handles various HTTP status codes", func(t *testing.T) {
|
t.Run("handles various HTTP status codes", func(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
errContains string
|
||||||
status int
|
status int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errContains string
|
|
||||||
}{
|
}{
|
||||||
{200, false, ""},
|
{status: 200, wantErr: false, errContains: ""},
|
||||||
{400, true, "400"},
|
{status: 400, wantErr: true, errContains: "400"},
|
||||||
{401, true, "401"},
|
{status: 401, wantErr: true, errContains: "401"},
|
||||||
{403, true, "403"},
|
{status: 403, wantErr: true, errContains: "403"},
|
||||||
{404, true, "404"},
|
{status: 404, wantErr: true, errContains: "404"},
|
||||||
{500, true, "500"},
|
{status: 500, wantErr: true, errContains: "500"},
|
||||||
{502, true, "502"},
|
{status: 502, wantErr: true, errContains: "502"},
|
||||||
{503, true, "503"},
|
{status: 503, wantErr: true, errContains: "503"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ import (
|
|||||||
// TestExchangeCodeForToken_Comprehensive tests the ExchangeCodeForToken function comprehensively
|
// TestExchangeCodeForToken_Comprehensive tests the ExchangeCodeForToken function comprehensively
|
||||||
func TestExchangeCodeForToken_Comprehensive(t *testing.T) {
|
func TestExchangeCodeForToken_Comprehensive(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
setupMock func(*httptest.Server) *TraefikOidc
|
||||||
|
validateFunc func(*testing.T, *TokenResponse, error)
|
||||||
name string
|
name string
|
||||||
grantType string
|
grantType string
|
||||||
code string
|
code string
|
||||||
redirectURL string
|
redirectURL string
|
||||||
codeVerifier string
|
codeVerifier string
|
||||||
setupMock func(*httptest.Server) *TraefikOidc
|
|
||||||
validateFunc func(*testing.T, *TokenResponse, error)
|
|
||||||
wantErr bool
|
|
||||||
expectedError string
|
expectedError string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "successful authorization code exchange",
|
name: "successful authorization code exchange",
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import (
|
|||||||
func TestGoroutineLeakPrevention_ContextCancellation(t *testing.T) {
|
func TestGoroutineLeakPrevention_ContextCancellation(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cancelAfter time.Duration
|
|
||||||
expectedLeaks int // Maximum expected goroutines after cleanup
|
|
||||||
description string
|
description string
|
||||||
|
cancelAfter time.Duration
|
||||||
|
expectedLeaks int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "immediate_cancellation",
|
name: "immediate_cancellation",
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
// TestInitializeMetadata tests the initializeMetadata function
|
// TestInitializeMetadata tests the initializeMetadata function
|
||||||
func TestInitializeMetadata(t *testing.T) {
|
func TestInitializeMetadata(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
providerURL string
|
|
||||||
setupMock func() *httptest.Server
|
setupMock func() *httptest.Server
|
||||||
validateFunc func(*testing.T, *TraefikOidc)
|
validateFunc func(*testing.T, *TraefikOidc)
|
||||||
|
name string
|
||||||
|
providerURL string
|
||||||
wantPanic bool
|
wantPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import (
|
|||||||
// TestGetNewTokenWithRefreshToken tests the GetNewTokenWithRefreshToken function
|
// TestGetNewTokenWithRefreshToken tests the GetNewTokenWithRefreshToken function
|
||||||
func TestGetNewTokenWithRefreshToken(t *testing.T) {
|
func TestGetNewTokenWithRefreshToken(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
refreshToken string
|
|
||||||
setupMock func(*httptest.Server) *TraefikOidc
|
setupMock func(*httptest.Server) *TraefikOidc
|
||||||
validateFunc func(*testing.T, *TokenResponse, error)
|
validateFunc func(*testing.T, *TokenResponse, error)
|
||||||
wantErr bool
|
name string
|
||||||
|
refreshToken string
|
||||||
expectedError string
|
expectedError string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "successful token refresh",
|
name: "successful token refresh",
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
// TestServeHTTP_ExcludedURLs tests the excluded URLs functionality
|
// TestServeHTTP_ExcludedURLs tests the excluded URLs functionality
|
||||||
func TestServeHTTP_ExcludedURLs(t *testing.T) {
|
func TestServeHTTP_ExcludedURLs(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
excludedURLs map[string]struct{}
|
||||||
name string
|
name string
|
||||||
path string
|
path string
|
||||||
excludedURLs map[string]struct{}
|
|
||||||
shouldBypass bool
|
shouldBypass bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -506,12 +506,12 @@ type MockSessionData struct {
|
|||||||
idToken string
|
idToken string
|
||||||
accessToken string
|
accessToken string
|
||||||
refreshToken string
|
refreshToken string
|
||||||
authenticated bool
|
|
||||||
isDirty bool
|
|
||||||
redirectCount int
|
|
||||||
csrf string
|
csrf string
|
||||||
nonce string
|
nonce string
|
||||||
codeVerifier string
|
codeVerifier string
|
||||||
|
redirectCount int
|
||||||
|
authenticated bool
|
||||||
|
isDirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockSessionData) GetEmail() string { return m.email }
|
func (m *MockSessionData) GetEmail() string { return m.email }
|
||||||
|
|||||||
+2
-2
@@ -81,11 +81,11 @@ func TestIsTestMode_DefaultBehavior(t *testing.T) {
|
|||||||
// TestVerifyAudience tests the verifyAudience function
|
// TestVerifyAudience tests the verifyAudience function
|
||||||
func TestVerifyAudience(t *testing.T) {
|
func TestVerifyAudience(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
tokenAudience interface{}
|
tokenAudience interface{}
|
||||||
|
name string
|
||||||
expectedAudience string
|
expectedAudience string
|
||||||
expectError bool
|
|
||||||
description string
|
description string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Audience matches",
|
name: "Audience matches",
|
||||||
|
|||||||
+2
-2
@@ -192,9 +192,9 @@ func (ts *TestSuite) Setup() {
|
|||||||
|
|
||||||
// MockJWKCache implements JWKCacheInterface
|
// MockJWKCache implements JWKCacheInterface
|
||||||
type MockJWKCache struct {
|
type MockJWKCache struct {
|
||||||
mu sync.RWMutex
|
|
||||||
JWKS *JWKSet
|
|
||||||
Err error
|
Err error
|
||||||
|
JWKS *JWKSet
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close is a no-op for the mock.
|
// Close is a no-op for the mock.
|
||||||
|
|||||||
+11
-11
@@ -42,27 +42,27 @@ func NewMemoryLeakFixesTestSuite() *MemoryLeakFixesTestSuite {
|
|||||||
|
|
||||||
// MemoryTestCase defines a memory leak test scenario
|
// MemoryTestCase defines a memory leak test scenario
|
||||||
type MemoryTestCase struct {
|
type MemoryTestCase struct {
|
||||||
name string
|
|
||||||
component string // "cache", "session", "token", "plugin", "pool"
|
|
||||||
scenario string // "concurrent", "longrunning", "stress", "lifecycle"
|
|
||||||
iterations int
|
|
||||||
concurrency int
|
|
||||||
setup func(*MemoryTestFramework) error
|
setup func(*MemoryTestFramework) error
|
||||||
execute func(*MemoryTestFramework) error
|
execute func(*MemoryTestFramework) error
|
||||||
validateLeak func(*testing.T, runtime.MemStats, runtime.MemStats)
|
validateLeak func(*testing.T, runtime.MemStats, runtime.MemStats)
|
||||||
cleanup func(*MemoryTestFramework) error
|
cleanup func(*MemoryTestFramework) error
|
||||||
|
name string
|
||||||
|
component string
|
||||||
|
scenario string
|
||||||
|
iterations int
|
||||||
|
concurrency int
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryTestFramework provides common test infrastructure for memory tests
|
// MemoryTestFramework provides common test infrastructure for memory tests
|
||||||
type MemoryTestFramework struct {
|
type MemoryTestFramework struct {
|
||||||
t *testing.T
|
|
||||||
cache CacheInterface
|
cache CacheInterface
|
||||||
|
ctx context.Context
|
||||||
|
t *testing.T
|
||||||
plugin *TraefikOidc
|
plugin *TraefikOidc
|
||||||
logger *Logger
|
logger *Logger
|
||||||
|
cancel context.CancelFunc
|
||||||
servers []*httptest.Server
|
servers []*httptest.Server
|
||||||
configs []*Config
|
configs []*Config
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMemoryTestFramework creates a new test framework instance
|
// NewMemoryTestFramework creates a new test framework instance
|
||||||
@@ -97,12 +97,12 @@ func (tf *MemoryTestFramework) Cleanup() {
|
|||||||
// ConsolidatedMemorySnapshot captures memory statistics at a point in time
|
// ConsolidatedMemorySnapshot captures memory statistics at a point in time
|
||||||
type ConsolidatedMemorySnapshot struct {
|
type ConsolidatedMemorySnapshot struct {
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
|
Description string
|
||||||
Alloc uint64
|
Alloc uint64
|
||||||
TotalAlloc uint64
|
TotalAlloc uint64
|
||||||
Sys uint64
|
Sys uint64
|
||||||
NumGC uint32
|
|
||||||
Goroutines int
|
Goroutines int
|
||||||
Description string
|
NumGC uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyNoGoroutineLeaks checks for goroutine leaks
|
// VerifyNoGoroutineLeaks checks for goroutine leaks
|
||||||
@@ -1601,8 +1601,8 @@ func TestMemoryLeakConsolidated(t *testing.T) {
|
|||||||
|
|
||||||
func TestGoroutineLeaks(t *testing.T) {
|
func TestGoroutineLeaks(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
|
||||||
test func(t *testing.T)
|
test func(t *testing.T)
|
||||||
|
name string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "cache_no_leak",
|
name: "cache_no_leak",
|
||||||
|
|||||||
+28
-38
@@ -10,30 +10,24 @@ import (
|
|||||||
|
|
||||||
// MemoryStats holds comprehensive memory statistics
|
// MemoryStats holds comprehensive memory statistics
|
||||||
type MemoryStats struct {
|
type MemoryStats struct {
|
||||||
// Go runtime memory stats
|
LastGCTime time.Time
|
||||||
HeapAllocBytes uint64 // bytes allocated and still in use
|
Timestamp time.Time
|
||||||
HeapSysBytes uint64 // bytes obtained from system
|
GCSysBytes uint64
|
||||||
HeapIdleBytes uint64 // bytes in idle (unused) spans
|
NumGoroutines int
|
||||||
HeapInuseBytes uint64 // bytes in in-use spans
|
HeapReleasedBytes uint64
|
||||||
HeapReleasedBytes uint64 // bytes released to the OS
|
HeapObjects uint64
|
||||||
HeapObjects uint64 // total number of allocated objects
|
StackInuseBytes uint64
|
||||||
StackInuseBytes uint64 // bytes in stack spans
|
StackSysBytes uint64
|
||||||
StackSysBytes uint64 // bytes obtained from system for stack
|
HeapAllocBytes uint64
|
||||||
GCSysBytes uint64 // bytes used for garbage collection system metadata
|
HeapInuseBytes uint64
|
||||||
NumGoroutines int // number of goroutines that currently exist
|
HeapIdleBytes uint64
|
||||||
LastGCTime time.Time // time of last garbage collection
|
SessionCount int
|
||||||
|
TaskCount int
|
||||||
// Application-specific memory tracking
|
CacheSize int64
|
||||||
SessionCount int // current number of sessions
|
ConnectionPools int
|
||||||
TaskCount int // current number of background tasks
|
MemoryPressure MemoryPressureLevel
|
||||||
CacheSize int64 // estimated cache memory usage
|
GCFrequency float64
|
||||||
ConnectionPools int // number of HTTP connection pools
|
HeapSysBytes uint64
|
||||||
|
|
||||||
// Memory pressure indicators
|
|
||||||
MemoryPressure MemoryPressureLevel // overall memory pressure level
|
|
||||||
GCFrequency float64 // garbage collections per minute
|
|
||||||
|
|
||||||
Timestamp time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryPressureLevel indicates the current memory pressure
|
// MemoryPressureLevel indicates the current memory pressure
|
||||||
@@ -66,22 +60,18 @@ func (mpl MemoryPressureLevel) String() string {
|
|||||||
|
|
||||||
// MemoryMonitor provides comprehensive memory monitoring and alerting
|
// MemoryMonitor provides comprehensive memory monitoring and alerting
|
||||||
type MemoryMonitor struct {
|
type MemoryMonitor struct {
|
||||||
logger *Logger
|
lastGCTime time.Time
|
||||||
mu sync.RWMutex
|
startTime time.Time
|
||||||
lastStats *MemoryStats
|
lastStats *MemoryStats
|
||||||
lastGCCount uint32
|
logger *Logger
|
||||||
lastGCTime time.Time
|
alertThresholds MemoryAlertThresholds
|
||||||
startTime time.Time
|
|
||||||
alertThresholds MemoryAlertThresholds
|
|
||||||
|
|
||||||
// Memory leak detection
|
|
||||||
baselineHeap uint64
|
|
||||||
heapGrowthRate float64 // bytes per second
|
|
||||||
suspiciousGrowth bool
|
|
||||||
|
|
||||||
// Goroutine tracking
|
|
||||||
baselineGoroutines int
|
baselineGoroutines int
|
||||||
|
baselineHeap uint64
|
||||||
|
heapGrowthRate float64
|
||||||
maxGoroutines int64
|
maxGoroutines int64
|
||||||
|
mu sync.RWMutex
|
||||||
|
lastGCCount uint32
|
||||||
|
suspiciousGrowth bool
|
||||||
goroutineLeakAlert bool
|
goroutineLeakAlert bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-52
@@ -19,31 +19,25 @@ import (
|
|||||||
|
|
||||||
// MockOAuthProvider simulates an OAuth/OIDC provider for testing
|
// MockOAuthProvider simulates an OAuth/OIDC provider for testing
|
||||||
type MockOAuthProvider struct {
|
type MockOAuthProvider struct {
|
||||||
TokenEndpoint string
|
TokenExchangeFunc func(grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error)
|
||||||
AuthEndpoint string
|
LastRequest *http.Request
|
||||||
JWKSEndpoint string
|
JWKSResponseFunc func() ([]byte, error)
|
||||||
RevokeEndpoint string
|
RevokeTokenFunc func(token, tokenType string) error
|
||||||
EndSessionEndpoint string
|
RefreshTokenFunc func(refreshToken string) (*TokenResponse, error)
|
||||||
|
EndSessionEndpoint string
|
||||||
// Configurable behaviors
|
TokenEndpoint string
|
||||||
TokenExchangeFunc func(grantType, codeOrToken, redirectURL, codeVerifier string) (*TokenResponse, error)
|
RevokeEndpoint string
|
||||||
RefreshTokenFunc func(refreshToken string) (*TokenResponse, error)
|
JWKSEndpoint string
|
||||||
RevokeTokenFunc func(token, tokenType string) error
|
AuthEndpoint string
|
||||||
JWKSResponseFunc func() ([]byte, error)
|
RequestHistory []*http.Request
|
||||||
|
LastRequestBody []byte
|
||||||
// Simulation flags
|
|
||||||
SimulateTimeout bool
|
|
||||||
SimulateRateLimit bool
|
|
||||||
SimulateServerError bool
|
|
||||||
TimeoutDuration time.Duration
|
TimeoutDuration time.Duration
|
||||||
ResponseDelay time.Duration
|
ResponseDelay time.Duration
|
||||||
|
mu sync.Mutex
|
||||||
// Request tracking
|
RequestCount int32
|
||||||
RequestCount int32
|
SimulateServerError bool
|
||||||
LastRequest *http.Request
|
SimulateRateLimit bool
|
||||||
LastRequestBody []byte
|
SimulateTimeout bool
|
||||||
RequestHistory []*http.Request
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockOAuthProvider creates a new mock OAuth provider with default endpoints
|
// NewMockOAuthProvider creates a new mock OAuth provider with default endpoints
|
||||||
@@ -236,22 +230,16 @@ func (m *MockOAuthProvider) Reset() {
|
|||||||
|
|
||||||
// MockSessionManager implements a mock session manager for testing
|
// MockSessionManager implements a mock session manager for testing
|
||||||
type MockSessionManager struct {
|
type MockSessionManager struct {
|
||||||
Sessions map[string]*SessionData
|
Sessions map[string]*SessionData
|
||||||
mu sync.RWMutex
|
|
||||||
|
|
||||||
// Configurable behaviors
|
|
||||||
GetSessionFunc func(r *http.Request) (*SessionData, error)
|
GetSessionFunc func(r *http.Request) (*SessionData, error)
|
||||||
SaveSessionFunc func(r *http.Request, w http.ResponseWriter, session *SessionData) error
|
SaveSessionFunc func(r *http.Request, w http.ResponseWriter, session *SessionData) error
|
||||||
DeleteSessionFunc func(r *http.Request, w http.ResponseWriter) error
|
DeleteSessionFunc func(r *http.Request, w http.ResponseWriter) error
|
||||||
|
mu sync.RWMutex
|
||||||
// Simulation flags
|
GetCallCount int32
|
||||||
SimulateError bool
|
SaveCallCount int32
|
||||||
SimulateNotFound bool
|
DeleteCallCount int32
|
||||||
|
SimulateError bool
|
||||||
// Tracking
|
SimulateNotFound bool
|
||||||
GetCallCount int32
|
|
||||||
SaveCallCount int32
|
|
||||||
DeleteCallCount int32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockSessionManager creates a new mock session manager
|
// NewMockSessionManager creates a new mock session manager
|
||||||
@@ -370,23 +358,16 @@ func (m *MockSessionManager) Reset() {
|
|||||||
|
|
||||||
// MockHTTPClient implements a mock HTTP client for testing
|
// MockHTTPClient implements a mock HTTP client for testing
|
||||||
type MockHTTPClient struct {
|
type MockHTTPClient struct {
|
||||||
// Response configuration
|
ResponseFunc func(req *http.Request) (*http.Response, error)
|
||||||
ResponseFunc func(req *http.Request) (*http.Response, error)
|
|
||||||
|
|
||||||
// Default response settings
|
|
||||||
DefaultStatusCode int
|
|
||||||
DefaultBody string
|
|
||||||
DefaultHeaders map[string]string
|
DefaultHeaders map[string]string
|
||||||
|
DefaultBody string
|
||||||
// Simulation flags
|
Requests []*http.Request
|
||||||
SimulateTimeout bool
|
RequestBodies [][]byte
|
||||||
SimulateError bool
|
DefaultStatusCode int
|
||||||
TimeoutDuration time.Duration
|
TimeoutDuration time.Duration
|
||||||
|
mu sync.Mutex
|
||||||
// Request tracking
|
SimulateTimeout bool
|
||||||
Requests []*http.Request
|
SimulateError bool
|
||||||
RequestBodies [][]byte
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockHTTPClient creates a new mock HTTP client
|
// NewMockHTTPClient creates a new mock HTTP client
|
||||||
|
|||||||
+25
-56
@@ -15,38 +15,19 @@ import (
|
|||||||
// It implements request coalescing, rate limiting, and circuit breaking
|
// It implements request coalescing, rate limiting, and circuit breaking
|
||||||
// specifically for token refresh operations.
|
// specifically for token refresh operations.
|
||||||
type RefreshCoordinator struct {
|
type RefreshCoordinator struct {
|
||||||
// inFlightRefreshes tracks active refresh operations by refresh token hash
|
inFlightRefreshes map[string]*refreshOperation
|
||||||
inFlightRefreshes map[string]*refreshOperation
|
cleanupTimers map[string]*time.Timer
|
||||||
// refreshMutex protects the inFlightRefreshes map
|
|
||||||
refreshMutex sync.RWMutex
|
|
||||||
|
|
||||||
// sessionRefreshAttempts tracks refresh attempts per session
|
|
||||||
sessionRefreshAttempts map[string]*refreshAttemptTracker
|
sessionRefreshAttempts map[string]*refreshAttemptTracker
|
||||||
// attemptsMutex protects sessionRefreshAttempts map
|
delayedCleanupQueue chan delayedCleanupItem
|
||||||
attemptsMutex sync.RWMutex
|
circuitBreaker *RefreshCircuitBreaker
|
||||||
|
metrics *RefreshMetrics
|
||||||
// Circuit breaker for refresh operations
|
logger *Logger
|
||||||
circuitBreaker *RefreshCircuitBreaker
|
stopChan chan struct{}
|
||||||
|
config RefreshCoordinatorConfig
|
||||||
// Configuration
|
wg sync.WaitGroup
|
||||||
config RefreshCoordinatorConfig
|
attemptsMutex sync.RWMutex
|
||||||
|
refreshMutex sync.RWMutex
|
||||||
// Metrics
|
cleanupTimerMu sync.Mutex
|
||||||
metrics *RefreshMetrics
|
|
||||||
|
|
||||||
// Logger
|
|
||||||
logger *Logger
|
|
||||||
|
|
||||||
// Cleanup goroutine control
|
|
||||||
stopChan chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
// delayedCleanupQueue stores items to be cleaned up after delay
|
|
||||||
// Uses a timer-based approach instead of spawning goroutines per cleanup
|
|
||||||
delayedCleanupQueue chan delayedCleanupItem
|
|
||||||
// cleanupTimerPool reuses timers to avoid goroutine-per-cleanup
|
|
||||||
cleanupTimerMu sync.Mutex
|
|
||||||
cleanupTimers map[string]*time.Timer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshCoordinatorConfig configures the refresh coordinator behavior
|
// RefreshCoordinatorConfig configures the refresh coordinator behavior
|
||||||
@@ -89,18 +70,12 @@ func DefaultRefreshCoordinatorConfig() RefreshCoordinatorConfig {
|
|||||||
|
|
||||||
// refreshOperation represents an in-flight refresh operation
|
// refreshOperation represents an in-flight refresh operation
|
||||||
type refreshOperation struct {
|
type refreshOperation struct {
|
||||||
// refreshToken being refreshed (for validation)
|
startTime time.Time
|
||||||
|
result *refreshResult
|
||||||
|
done chan struct{}
|
||||||
refreshToken string
|
refreshToken string
|
||||||
// result stores the final result
|
mutex sync.RWMutex
|
||||||
result *refreshResult
|
waiterCount int32
|
||||||
// done signals when the operation is complete
|
|
||||||
done chan struct{}
|
|
||||||
// startTime tracks when the operation started
|
|
||||||
startTime time.Time
|
|
||||||
// waiterCount tracks number of goroutines waiting
|
|
||||||
waiterCount int32
|
|
||||||
// mutex protects the result field
|
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// refreshResult contains the result of a refresh operation
|
// refreshResult contains the result of a refresh operation
|
||||||
@@ -112,18 +87,12 @@ type refreshResult struct {
|
|||||||
|
|
||||||
// refreshAttemptTracker tracks refresh attempts for a session
|
// refreshAttemptTracker tracks refresh attempts for a session
|
||||||
type refreshAttemptTracker struct {
|
type refreshAttemptTracker struct {
|
||||||
// attempts counts refresh attempts in current window
|
lastAttemptTime time.Time
|
||||||
attempts int32
|
windowStartTime time.Time
|
||||||
// lastAttemptTime is the timestamp of the last attempt
|
cooldownEndTime time.Time
|
||||||
lastAttemptTime time.Time
|
attempts int32
|
||||||
// windowStartTime is when the current tracking window started
|
|
||||||
windowStartTime time.Time
|
|
||||||
// inCooldown indicates if this session is in cooldown
|
|
||||||
inCooldown bool
|
|
||||||
// cooldownEndTime is when cooldown period ends
|
|
||||||
cooldownEndTime time.Time
|
|
||||||
// consecutiveFailures tracks consecutive refresh failures
|
|
||||||
consecutiveFailures int32
|
consecutiveFailures int32
|
||||||
|
inCooldown bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshMetrics tracks coordinator performance metrics
|
// RefreshMetrics tracks coordinator performance metrics
|
||||||
@@ -140,18 +109,18 @@ type RefreshMetrics struct {
|
|||||||
|
|
||||||
// delayedCleanupItem represents an item scheduled for delayed cleanup
|
// delayedCleanupItem represents an item scheduled for delayed cleanup
|
||||||
type delayedCleanupItem struct {
|
type delayedCleanupItem struct {
|
||||||
tokenHash string
|
|
||||||
cleanupAt time.Time
|
cleanupAt time.Time
|
||||||
|
tokenHash string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshCircuitBreaker implements a circuit breaker specifically for refresh operations
|
// RefreshCircuitBreaker implements a circuit breaker specifically for refresh operations
|
||||||
type RefreshCircuitBreaker struct {
|
type RefreshCircuitBreaker struct {
|
||||||
state int32 // 0=closed, 1=open, 2=half-open
|
|
||||||
failures int32
|
|
||||||
lastFailureTime time.Time
|
lastFailureTime time.Time
|
||||||
lastSuccessTime time.Time
|
lastSuccessTime time.Time
|
||||||
config RefreshCircuitBreakerConfig
|
config RefreshCircuitBreakerConfig
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
state int32
|
||||||
|
failures int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshCircuitBreakerConfig configures the refresh circuit breaker
|
// RefreshCircuitBreakerConfig configures the refresh circuit breaker
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ func testIssue53ReverseProxyHTTPS(t *testing.T) {
|
|||||||
session.SetEmail("[email protected]")
|
session.SetEmail("[email protected]")
|
||||||
// Azure may use opaque access tokens
|
// Azure may use opaque access tokens
|
||||||
session.SetAccessToken("opaque-azure-access-token")
|
session.SetAccessToken("opaque-azure-access-token")
|
||||||
session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ")
|
session.SetIDToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ") // trufflehog:ignore
|
||||||
session.SetRefreshToken("azure-refresh-token")
|
session.SetRefreshToken("azure-refresh-token")
|
||||||
|
|
||||||
// Save with proper security
|
// Save with proper security
|
||||||
@@ -178,9 +178,9 @@ func testIssue53SameSiteCookies(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
proto string
|
proto string
|
||||||
expectedSecure bool
|
|
||||||
expectedSameSite http.SameSite
|
|
||||||
description string
|
description string
|
||||||
|
expectedSameSite http.SameSite
|
||||||
|
expectedSecure bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "HTTPS via proxy",
|
name: "HTTPS via proxy",
|
||||||
@@ -240,9 +240,9 @@ func testIssue60MissingClaimFields(t *testing.T) {
|
|||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
description string
|
||||||
headers []traefikoidc.TemplatedHeader
|
headers []traefikoidc.TemplatedHeader
|
||||||
shouldValidate bool
|
shouldValidate bool
|
||||||
description string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Direct claim access",
|
name: "Direct claim access",
|
||||||
|
|||||||
+555
-23
@@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -66,23 +67,160 @@ func generateSecureRandomString(length int) (string, error) {
|
|||||||
// These are appended to the cookiePrefix to create full cookie names
|
// These are appended to the cookiePrefix to create full cookie names
|
||||||
// #nosec G101 -- These are cookie name suffixes, not hardcoded credentials
|
// #nosec G101 -- These are cookie name suffixes, not hardcoded credentials
|
||||||
const (
|
const (
|
||||||
mainCookieSuffix = "m"
|
mainCookieSuffix = "m"
|
||||||
accessTokenSuffix = "a"
|
accessTokenSuffix = "a"
|
||||||
refreshTokenSuffix = "r"
|
refreshTokenSuffix = "r"
|
||||||
idTokenSuffix = "id"
|
idTokenSuffix = "id"
|
||||||
defaultCookiePrefix = "_oidc_raczylo_"
|
combinedCookieSuffix = "s" // Combined session cookie suffix
|
||||||
|
defaultCookiePrefix = "_oidc_raczylo_"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxBrowserCookieSize = 3500
|
// maxCookieSize is the maximum raw data size per chunk before securecookie encoding.
|
||||||
|
//
|
||||||
|
// Browser cookie limit: 4096 bytes
|
||||||
|
// Securecookie overhead: ~2x (gob + AES encryption + MAC + double base64)
|
||||||
|
// Safety headroom: 1000 bytes for cookie metadata, headers, edge cases
|
||||||
|
//
|
||||||
|
// Math: 1400 raw → ~2800 encoded → safely under (4096 - 1000 = 3096) limit
|
||||||
|
maxCookieSize = 1400
|
||||||
|
|
||||||
maxCookieSize = 1200
|
// maxCombinedChunks is the maximum number of chunks allowed for combined session
|
||||||
|
maxCombinedChunks = 10
|
||||||
|
|
||||||
absoluteSessionTimeout = 24 * time.Hour
|
absoluteSessionTimeout = 24 * time.Hour
|
||||||
|
|
||||||
minEncryptionKeyLength = 32
|
minEncryptionKeyLength = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// combinedSessionPayload is the JSON structure for combined cookie storage.
|
||||||
|
// Uses short field names to minimize size.
|
||||||
|
type combinedSessionPayload struct {
|
||||||
|
X map[string]interface{} `json:"x,omitempty"`
|
||||||
|
A string `json:"a,omitempty"`
|
||||||
|
R string `json:"r,omitempty"`
|
||||||
|
I string `json:"i,omitempty"`
|
||||||
|
E string `json:"e,omitempty"`
|
||||||
|
Cs string `json:"cs,omitempty"`
|
||||||
|
N string `json:"n,omitempty"`
|
||||||
|
Cv string `json:"cv,omitempty"`
|
||||||
|
Ip string `json:"ip,omitempty"`
|
||||||
|
Ca int64 `json:"ca,omitempty"`
|
||||||
|
Rc int `json:"rc,omitempty"`
|
||||||
|
Au bool `json:"au,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// knownSessionKeys are the standard keys that are handled explicitly in the combined payload.
|
||||||
|
// All other mainSession.Values keys are stored in the X (extra) field.
|
||||||
|
var knownSessionKeys = map[string]bool{
|
||||||
|
"access_token": true,
|
||||||
|
"refresh_token": true,
|
||||||
|
"id_token": true,
|
||||||
|
"email": true,
|
||||||
|
"authenticated": true,
|
||||||
|
"csrf": true,
|
||||||
|
"nonce": true,
|
||||||
|
"code_verifier": true,
|
||||||
|
"incoming_path": true,
|
||||||
|
"created_at": true,
|
||||||
|
"redirect_count": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// compressCombinedPayload compresses the combined session payload using gzip.
|
||||||
|
// It serializes the payload to JSON, compresses it, and returns base64-encoded data.
|
||||||
|
// Returns the compressed string and any error encountered.
|
||||||
|
func compressCombinedPayload(payload *combinedSessionPayload) (string, error) {
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to marshal combined payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
gz := gzip.NewWriter(&buf)
|
||||||
|
if _, err := gz.Write(jsonData); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to compress combined payload: %w", err)
|
||||||
|
}
|
||||||
|
if err := gz.Close(); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to close gzip writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
compressed := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||||
|
return compressed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decompressCombinedPayload decompresses a base64+gzip encoded combined session payload.
|
||||||
|
// Returns the deserialized payload and any error encountered.
|
||||||
|
func decompressCombinedPayload(compressed string) (*combinedSessionPayload, error) {
|
||||||
|
if compressed == "" {
|
||||||
|
return nil, fmt.Errorf("empty compressed data")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.StdEncoding.DecodeString(compressed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode base64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gr, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||||
|
}
|
||||||
|
defer gr.Close()
|
||||||
|
|
||||||
|
// Limit decompressed size to prevent zip bombs
|
||||||
|
limitedReader := io.LimitReader(gr, 512*1024) // 512KB max
|
||||||
|
decompressed, err := io.ReadAll(limitedReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decompress: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload combinedSessionPayload
|
||||||
|
if err := json.Unmarshal(decompressed, &payload); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal combined payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitCombinedIntoChunks splits compressed data into chunks of maxCookieSize.
|
||||||
|
// Returns the chunks and the total number of chunks.
|
||||||
|
func splitCombinedIntoChunks(data string, chunkSize int) []string {
|
||||||
|
if len(data) <= chunkSize {
|
||||||
|
return []string{data}
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunks []string
|
||||||
|
for i := 0; i < len(data); i += chunkSize {
|
||||||
|
end := i + chunkSize
|
||||||
|
if end > len(data) {
|
||||||
|
end = len(data)
|
||||||
|
}
|
||||||
|
chunks = append(chunks, data[i:end])
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
// assembleCombinedChunks reassembles chunks back into the original compressed data.
|
||||||
|
// It reads chunks from session values in order (chunk_0, chunk_1, etc.).
|
||||||
|
func assembleCombinedChunks(sessions []*sessions.Session) string {
|
||||||
|
if len(sessions) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
for i := 0; i < len(sessions); i++ {
|
||||||
|
session := sessions[i]
|
||||||
|
if session == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
chunk, ok := session.Values["d"].(string) // "d" for data
|
||||||
|
if !ok || chunk == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parts = append(parts, chunk)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
// compressToken compresses a JWT token using gzip compression if beneficial.
|
// compressToken compresses a JWT token using gzip compression if beneficial.
|
||||||
// It validates the token format, attempts compression, and verifies the compressed
|
// It validates the token format, attempts compression, and verifies the compressed
|
||||||
// data can be decompressed correctly. Only compresses if it reduces size.
|
// data can be decompressed correctly. Only compresses if it reduces size.
|
||||||
@@ -236,22 +374,22 @@ func decompressTokenInternal(compressed string) string {
|
|||||||
// session object reuse and supports both HTTP and HTTPS schemes.
|
// session object reuse and supports both HTTP and HTTPS schemes.
|
||||||
type SessionManager struct {
|
type SessionManager struct {
|
||||||
sessionPool sync.Pool
|
sessionPool sync.Pool
|
||||||
|
ctx context.Context
|
||||||
store sessions.Store
|
store sessions.Store
|
||||||
logger *Logger
|
logger *Logger
|
||||||
chunkManager *ChunkManager
|
chunkManager *ChunkManager
|
||||||
cookieDomain string
|
|
||||||
cookiePrefix string // Prefix for cookie names (default: "_oidc_raczylo_")
|
|
||||||
sessionMaxAge time.Duration // Maximum session age (default: 24 hours)
|
|
||||||
cleanupMutex sync.RWMutex
|
|
||||||
forceHTTPS bool
|
|
||||||
cleanupDone bool
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
memoryMonitor *TaskMemoryMonitor
|
memoryMonitor *TaskMemoryMonitor
|
||||||
|
cancel context.CancelFunc
|
||||||
|
cookieDomain string
|
||||||
|
cookiePrefix string
|
||||||
|
sessionMaxAge time.Duration
|
||||||
activeSessions int64
|
activeSessions int64
|
||||||
poolHits int64
|
poolHits int64
|
||||||
poolMisses int64
|
poolMisses int64
|
||||||
|
cleanupMutex sync.RWMutex
|
||||||
shutdownOnce sync.Once
|
shutdownOnce sync.Once
|
||||||
|
forceHTTPS bool
|
||||||
|
cleanupDone bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSessionManager creates a new SessionManager instance with secure defaults.
|
// NewSessionManager creates a new SessionManager instance with secure defaults.
|
||||||
@@ -312,6 +450,8 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain strin
|
|||||||
accessTokenChunks: make(map[int]*sessions.Session),
|
accessTokenChunks: make(map[int]*sessions.Session),
|
||||||
refreshTokenChunks: make(map[int]*sessions.Session),
|
refreshTokenChunks: make(map[int]*sessions.Session),
|
||||||
idTokenChunks: make(map[int]*sessions.Session),
|
idTokenChunks: make(map[int]*sessions.Session),
|
||||||
|
combinedChunks: make(map[int]*sessions.Session),
|
||||||
|
useCombinedStorage: true, // Use combined storage by default for new sessions
|
||||||
refreshMutex: sync.Mutex{},
|
refreshMutex: sync.Mutex{},
|
||||||
sessionMutex: sync.RWMutex{},
|
sessionMutex: sync.RWMutex{},
|
||||||
dirty: false,
|
dirty: false,
|
||||||
@@ -349,6 +489,17 @@ func (sm *SessionManager) idTokenCookieName() string {
|
|||||||
return sm.cookiePrefix + idTokenSuffix
|
return sm.cookiePrefix + idTokenSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// combinedCookieName returns the combined session cookie base name with the configured prefix
|
||||||
|
// Chunk cookies are named: prefix + "s" + "_" + chunkIndex (e.g., "_oidc_raczylo_s_0")
|
||||||
|
func (sm *SessionManager) combinedCookieName() string {
|
||||||
|
return sm.cookiePrefix + combinedCookieSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
// combinedChunkCookieName returns the name for a specific combined session chunk
|
||||||
|
func (sm *SessionManager) combinedChunkCookieName(chunkIndex int) string {
|
||||||
|
return fmt.Sprintf("%s_%d", sm.combinedCookieName(), chunkIndex)
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the SessionManager and all its background tasks
|
// Shutdown gracefully shuts down the SessionManager and all its background tasks
|
||||||
func (sm *SessionManager) Shutdown() error {
|
func (sm *SessionManager) Shutdown() error {
|
||||||
var shutdownErr error
|
var shutdownErr error
|
||||||
@@ -650,7 +801,7 @@ func (sm *SessionManager) GetSessionMetrics() map[string]interface{} {
|
|||||||
metrics["force_https"] = sm.forceHTTPS
|
metrics["force_https"] = sm.forceHTTPS
|
||||||
metrics["absolute_timeout_hours"] = sm.sessionMaxAge.Hours()
|
metrics["absolute_timeout_hours"] = sm.sessionMaxAge.Hours()
|
||||||
metrics["max_cookie_size"] = maxCookieSize
|
metrics["max_cookie_size"] = maxCookieSize
|
||||||
metrics["max_browser_cookie_size"] = maxBrowserCookieSize
|
metrics["max_encoded_cookie_size"] = maxCookieSize * 2 // ~2x encoding overhead
|
||||||
|
|
||||||
if cookieStore, ok := sm.store.(*sessions.CookieStore); ok && len(cookieStore.Codecs) > 0 {
|
if cookieStore, ok := sm.store.(*sessions.CookieStore); ok && len(cookieStore.Codecs) > 0 {
|
||||||
metrics["has_encryption"] = true
|
metrics["has_encryption"] = true
|
||||||
@@ -824,9 +975,10 @@ func (sm *SessionManager) CleanupOldCookies(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSession retrieves or creates session data from the HTTP request.
|
// GetSession retrieves or creates session data from the HTTP request.
|
||||||
// It loads the main session and all token chunk sessions, performing validation
|
// It first tries to load from combined cookies (new format), falling back to legacy
|
||||||
// and timeout checks. The returned session must be explicitly returned to the pool
|
// cookies if combined cookies don't exist. Performs validation and timeout checks.
|
||||||
// by calling returnToPoolSafely() to prevent memory leaks.
|
// The returned session must be explicitly returned to the pool by calling
|
||||||
|
// returnToPoolSafely() to prevent memory leaks.
|
||||||
// MEMORY LEAK FIX: Session is NOT returned to pool here - caller must call ReturnToPool() when done.
|
// MEMORY LEAK FIX: Session is NOT returned to pool here - caller must call ReturnToPool() when done.
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - r: The HTTP request containing session cookies.
|
// - r: The HTTP request containing session cookies.
|
||||||
@@ -853,6 +1005,26 @@ func (sm *SessionManager) GetSession(r *http.Request) (*SessionData, error) {
|
|||||||
return nil, fmt.Errorf("%s: %w", message, err)
|
return nil, fmt.Errorf("%s: %w", message, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to load from combined cookies first (new format)
|
||||||
|
if sm.loadFromCombinedCookies(r, sessionData) {
|
||||||
|
sessionData.useCombinedStorage = true
|
||||||
|
sm.logger.Debug("Loaded session from combined cookies")
|
||||||
|
|
||||||
|
// Check session timeout
|
||||||
|
if sessionData.getCreatedAtUnsafe() > 0 {
|
||||||
|
if time.Since(time.Unix(sessionData.getCreatedAtUnsafe(), 0)) > sm.sessionMaxAge {
|
||||||
|
_ = sessionData.Clear(r, nil) // Safe to ignore: session is being invalidated
|
||||||
|
return handleError(fmt.Errorf("session timeout"), "session expired")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to legacy cookies
|
||||||
|
sessionData.useCombinedStorage = false
|
||||||
|
sm.logger.Debug("Loading session from legacy cookies")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
sessionData.mainSession, err = sm.store.Get(r, sm.mainCookieName())
|
sessionData.mainSession, err = sm.store.Get(r, sm.mainCookieName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -895,9 +1067,94 @@ func (sm *SessionManager) GetSession(r *http.Request) (*SessionData, error) {
|
|||||||
sm.getTokenChunkSessions(r, sm.refreshTokenCookieName(), sessionData.refreshTokenChunks)
|
sm.getTokenChunkSessions(r, sm.refreshTokenCookieName(), sessionData.refreshTokenChunks)
|
||||||
sm.getTokenChunkSessions(r, sm.idTokenCookieName(), sessionData.idTokenChunks)
|
sm.getTokenChunkSessions(r, sm.idTokenCookieName(), sessionData.idTokenChunks)
|
||||||
|
|
||||||
|
// If legacy session has data, migrate to combined storage on next save
|
||||||
|
if !sessionData.mainSession.IsNew {
|
||||||
|
sessionData.useCombinedStorage = true
|
||||||
|
sessionData.dirty = true // Mark dirty to trigger migration on save
|
||||||
|
sm.logger.Debug("Legacy session found, will migrate to combined storage on save")
|
||||||
|
}
|
||||||
|
|
||||||
return sessionData, nil
|
return sessionData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadFromCombinedCookies attempts to load session data from combined cookies.
|
||||||
|
// Returns true if combined cookies were found and successfully loaded.
|
||||||
|
func (sm *SessionManager) loadFromCombinedCookies(r *http.Request, sessionData *SessionData) bool {
|
||||||
|
// Check if first combined chunk exists
|
||||||
|
firstChunk, err := sm.store.Get(r, sm.combinedChunkCookieName(0))
|
||||||
|
if err != nil || firstChunk.IsNew {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total chunk count from first chunk
|
||||||
|
totalChunks, ok := firstChunk.Values["n"].(int)
|
||||||
|
if !ok || totalChunks < 1 || totalChunks > maxCombinedChunks {
|
||||||
|
sm.logger.Debugf("Invalid combined cookie chunk count: %v", firstChunk.Values["n"])
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all chunks
|
||||||
|
chunkSessions := make([]*sessions.Session, totalChunks)
|
||||||
|
chunkSessions[0] = firstChunk
|
||||||
|
sessionData.combinedChunks[0] = firstChunk
|
||||||
|
|
||||||
|
for i := 1; i < totalChunks; i++ {
|
||||||
|
chunk, err := sm.store.Get(r, sm.combinedChunkCookieName(i))
|
||||||
|
if err != nil || chunk.IsNew {
|
||||||
|
sm.logger.Debugf("Missing combined cookie chunk %d", i)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
chunkSessions[i] = chunk
|
||||||
|
sessionData.combinedChunks[i] = chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble and decompress
|
||||||
|
compressed := assembleCombinedChunks(chunkSessions)
|
||||||
|
if compressed == "" {
|
||||||
|
sm.logger.Debug("Failed to assemble combined chunks")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := decompressCombinedPayload(compressed)
|
||||||
|
if err != nil {
|
||||||
|
sm.logger.Debugf("Failed to decompress combined payload: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hydrate the legacy session objects for compatibility with existing getter methods
|
||||||
|
// We need to initialize them even though we're using combined storage
|
||||||
|
sessionData.mainSession, _ = sm.store.Get(r, sm.mainCookieName())
|
||||||
|
sessionData.accessSession, _ = sm.store.Get(r, sm.accessTokenCookieName())
|
||||||
|
sessionData.refreshSession, _ = sm.store.Get(r, sm.refreshTokenCookieName())
|
||||||
|
sessionData.idTokenSession, _ = sm.store.Get(r, sm.idTokenCookieName())
|
||||||
|
|
||||||
|
// Populate legacy session values from combined payload
|
||||||
|
sessionData.mainSession.Values["email"] = payload.E
|
||||||
|
sessionData.mainSession.Values["authenticated"] = payload.Au
|
||||||
|
sessionData.mainSession.Values["csrf"] = payload.Cs
|
||||||
|
sessionData.mainSession.Values["nonce"] = payload.N
|
||||||
|
sessionData.mainSession.Values["code_verifier"] = payload.Cv
|
||||||
|
sessionData.mainSession.Values["incoming_path"] = payload.Ip
|
||||||
|
sessionData.mainSession.Values["created_at"] = payload.Ca
|
||||||
|
sessionData.mainSession.Values["redirect_count"] = payload.Rc
|
||||||
|
|
||||||
|
// Restore extra custom session values
|
||||||
|
for key, val := range payload.X {
|
||||||
|
sessionData.mainSession.Values[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionData.accessSession.Values["token"] = payload.A
|
||||||
|
sessionData.accessSession.Values["compressed"] = false
|
||||||
|
|
||||||
|
sessionData.refreshSession.Values["token"] = payload.R
|
||||||
|
sessionData.refreshSession.Values["compressed"] = false
|
||||||
|
|
||||||
|
sessionData.idTokenSession.Values["token"] = payload.I
|
||||||
|
sessionData.idTokenSession.Values["compressed"] = false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// getTokenChunkSessions loads all available token chunk sessions for a given token type.
|
// getTokenChunkSessions loads all available token chunk sessions for a given token type.
|
||||||
// It iterates through numbered chunk sessions until no more are found,
|
// It iterates through numbered chunk sessions until no more are found,
|
||||||
// populating the provided chunks map with the loaded sessions.
|
// populating the provided chunks map with the loaded sessions.
|
||||||
@@ -920,11 +1177,13 @@ func (sm *SessionManager) getTokenChunkSessions(r *http.Request, baseName string
|
|||||||
// SessionData represents a user's authentication session with comprehensive token management.
|
// SessionData represents a user's authentication session with comprehensive token management.
|
||||||
// It handles main session data and supports large tokens that need to be
|
// It handles main session data and supports large tokens that need to be
|
||||||
// split across multiple cookies due to browser size limitations.
|
// split across multiple cookies due to browser size limitations.
|
||||||
|
// Supports both legacy (separate cookies) and combined (single compressed cookie) storage.
|
||||||
type SessionData struct {
|
type SessionData struct {
|
||||||
manager *SessionManager
|
manager *SessionManager
|
||||||
|
|
||||||
request *http.Request
|
request *http.Request
|
||||||
|
|
||||||
|
// Legacy storage (kept for backward compatibility during migration)
|
||||||
mainSession *sessions.Session
|
mainSession *sessions.Session
|
||||||
|
|
||||||
accessSession *sessions.Session
|
accessSession *sessions.Session
|
||||||
@@ -939,6 +1198,12 @@ type SessionData struct {
|
|||||||
|
|
||||||
idTokenChunks map[int]*sessions.Session
|
idTokenChunks map[int]*sessions.Session
|
||||||
|
|
||||||
|
// Combined storage (new approach - single compressed cookie)
|
||||||
|
combinedChunks map[int]*sessions.Session
|
||||||
|
|
||||||
|
// useCombinedStorage indicates whether to use the new combined storage format
|
||||||
|
useCombinedStorage bool
|
||||||
|
|
||||||
refreshMutex sync.Mutex
|
refreshMutex sync.Mutex
|
||||||
|
|
||||||
sessionMutex sync.RWMutex
|
sessionMutex sync.RWMutex
|
||||||
@@ -966,6 +1231,7 @@ func (sd *SessionData) MarkDirty() {
|
|||||||
// Save persists all session data including main session and token chunks.
|
// Save persists all session data including main session and token chunks.
|
||||||
// It applies security options, saves all session components, and handles
|
// It applies security options, saves all session components, and handles
|
||||||
// errors gracefully by continuing to save other components even if one fails.
|
// errors gracefully by continuing to save other components even if one fails.
|
||||||
|
// Uses combined cookie storage for efficiency when useCombinedStorage is true.
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - r: The HTTP request context for security option configuration.
|
// - r: The HTTP request context for security option configuration.
|
||||||
// - w: The HTTP response writer for setting session cookies.
|
// - w: The HTTP response writer for setting session cookies.
|
||||||
@@ -978,6 +1244,117 @@ func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error {
|
|||||||
options := sd.manager.getSessionOptions(isSecure)
|
options := sd.manager.getSessionOptions(isSecure)
|
||||||
options = sd.manager.EnhanceSessionSecurity(options, r)
|
options = sd.manager.EnhanceSessionSecurity(options, r)
|
||||||
|
|
||||||
|
// Use combined storage for new sessions
|
||||||
|
if sd.useCombinedStorage {
|
||||||
|
return sd.saveCombined(r, w, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy storage path (for backward compatibility)
|
||||||
|
return sd.saveLegacy(r, w, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveCombined saves all session data in a single compressed, chunked cookie.
|
||||||
|
// This reduces cookie count and total size through combined compression.
|
||||||
|
func (sd *SessionData) saveCombined(r *http.Request, w http.ResponseWriter, options *sessions.Options) error {
|
||||||
|
// Build the combined payload
|
||||||
|
payload := &combinedSessionPayload{
|
||||||
|
A: sd.getAccessTokenUnsafe(),
|
||||||
|
R: sd.getRefreshTokenUnsafe(),
|
||||||
|
I: sd.getIDTokenUnsafe(),
|
||||||
|
E: sd.getEmailUnsafe(),
|
||||||
|
Au: sd.getAuthenticatedUnsafe(),
|
||||||
|
Cs: sd.getCSRFUnsafe(),
|
||||||
|
N: sd.getNonceUnsafe(),
|
||||||
|
Cv: sd.getCodeVerifierUnsafe(),
|
||||||
|
Ip: sd.getIncomingPathUnsafe(),
|
||||||
|
Ca: sd.getCreatedAtUnsafe(),
|
||||||
|
Rc: sd.getRedirectCountUnsafe(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect extra session values not handled by the standard fields
|
||||||
|
sd.sessionMutex.RLock()
|
||||||
|
if sd.mainSession != nil && len(sd.mainSession.Values) > 0 {
|
||||||
|
extra := make(map[string]interface{})
|
||||||
|
for key, val := range sd.mainSession.Values {
|
||||||
|
keyStr, ok := key.(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip known session keys that are already in the payload
|
||||||
|
if knownSessionKeys[keyStr] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Store the extra value (must be JSON-serializable)
|
||||||
|
extra[keyStr] = val
|
||||||
|
}
|
||||||
|
if len(extra) > 0 {
|
||||||
|
payload.X = extra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sd.sessionMutex.RUnlock()
|
||||||
|
|
||||||
|
// Compress the payload
|
||||||
|
compressed, err := compressCombinedPayload(payload)
|
||||||
|
if err != nil {
|
||||||
|
sd.manager.logger.Errorf("Failed to compress combined payload: %v", err)
|
||||||
|
// Fall back to legacy storage on compression failure
|
||||||
|
return sd.saveLegacy(r, w, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
sd.manager.logger.Debugf("Combined session: raw payload compressed to %d bytes", len(compressed))
|
||||||
|
|
||||||
|
// Split into chunks
|
||||||
|
chunks := splitCombinedIntoChunks(compressed, maxCookieSize)
|
||||||
|
if len(chunks) > maxCombinedChunks {
|
||||||
|
sd.manager.logger.Errorf("Combined session requires %d chunks, exceeds max %d", len(chunks), maxCombinedChunks)
|
||||||
|
return fmt.Errorf("session data too large: requires %d chunks, max is %d", len(chunks), maxCombinedChunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
sd.manager.logger.Debugf("Combined session split into %d chunks", len(chunks))
|
||||||
|
|
||||||
|
var firstErr error
|
||||||
|
|
||||||
|
// Save each chunk
|
||||||
|
for i, chunkData := range chunks {
|
||||||
|
cookieName := sd.manager.combinedChunkCookieName(i)
|
||||||
|
session, err := sd.manager.store.Get(r, cookieName)
|
||||||
|
if err != nil {
|
||||||
|
sd.manager.logger.Errorf("Failed to get combined chunk session %s: %v", cookieName, err)
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Values["d"] = chunkData // "d" for data
|
||||||
|
session.Values["n"] = len(chunks) // "n" for total number of chunks
|
||||||
|
session.Values["i"] = i // "i" for index
|
||||||
|
session.Options = options
|
||||||
|
|
||||||
|
if err := session.Save(r, w); err != nil {
|
||||||
|
sd.manager.logger.Errorf("Failed to save combined chunk %d: %v", i, err)
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sd.combinedChunks[i] = session
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire old combined chunks that are no longer needed
|
||||||
|
sd.expireOldCombinedChunks(r, w, options, len(chunks))
|
||||||
|
|
||||||
|
// Expire legacy cookies if they exist (migration)
|
||||||
|
sd.expireLegacyCookies(r, w, options)
|
||||||
|
|
||||||
|
if firstErr == nil {
|
||||||
|
sd.dirty = false
|
||||||
|
}
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveLegacy saves session data using the old separate cookie approach.
|
||||||
|
// Kept for backward compatibility during migration.
|
||||||
|
func (sd *SessionData) saveLegacy(r *http.Request, w http.ResponseWriter, options *sessions.Options) error {
|
||||||
sd.mainSession.Options = options
|
sd.mainSession.Options = options
|
||||||
sd.accessSession.Options = options
|
sd.accessSession.Options = options
|
||||||
sd.refreshSession.Options = options
|
sd.refreshSession.Options = options
|
||||||
@@ -1002,11 +1379,8 @@ func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveOrLogError(sd.mainSession, "main")
|
saveOrLogError(sd.mainSession, "main")
|
||||||
|
|
||||||
saveOrLogError(sd.accessSession, "access token")
|
saveOrLogError(sd.accessSession, "access token")
|
||||||
|
|
||||||
saveOrLogError(sd.refreshSession, "refresh token")
|
saveOrLogError(sd.refreshSession, "refresh token")
|
||||||
|
|
||||||
saveOrLogError(sd.idTokenSession, "ID token")
|
saveOrLogError(sd.idTokenSession, "ID token")
|
||||||
|
|
||||||
for i, sessionChunk := range sd.accessTokenChunks {
|
for i, sessionChunk := range sd.accessTokenChunks {
|
||||||
@@ -1030,6 +1404,84 @@ func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error {
|
|||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// expireOldCombinedChunks expires combined cookie chunks that are no longer needed.
|
||||||
|
func (sd *SessionData) expireOldCombinedChunks(r *http.Request, w http.ResponseWriter, options *sessions.Options, currentChunks int) {
|
||||||
|
// Expire chunks beyond the current count
|
||||||
|
for i := currentChunks; i < maxCombinedChunks; i++ {
|
||||||
|
cookieName := sd.manager.combinedChunkCookieName(i)
|
||||||
|
session, err := sd.manager.store.Get(r, cookieName)
|
||||||
|
if err != nil || session.IsNew {
|
||||||
|
// No more old chunks
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Expire this chunk
|
||||||
|
expireOptions := *options
|
||||||
|
expireOptions.MaxAge = -1
|
||||||
|
session.Options = &expireOptions
|
||||||
|
for k := range session.Values {
|
||||||
|
delete(session.Values, k)
|
||||||
|
}
|
||||||
|
if err := session.Save(r, w); err != nil {
|
||||||
|
sd.manager.logger.Debugf("Failed to expire old combined chunk %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expireLegacyCookies expires old legacy format cookies during migration.
|
||||||
|
func (sd *SessionData) expireLegacyCookies(r *http.Request, w http.ResponseWriter, options *sessions.Options) {
|
||||||
|
expireOptions := *options
|
||||||
|
expireOptions.MaxAge = -1
|
||||||
|
|
||||||
|
// Helper to expire a legacy session cookie without clearing in-memory values
|
||||||
|
// IMPORTANT: We must NOT clear values from sessions that sd is holding,
|
||||||
|
// as store.Get() returns the same cached session object
|
||||||
|
expireLegacyChunk := func(cookieName string) {
|
||||||
|
session, err := sd.manager.store.Get(r, cookieName)
|
||||||
|
if err != nil || session.IsNew {
|
||||||
|
return // Cookie doesn't exist
|
||||||
|
}
|
||||||
|
session.Options = &expireOptions
|
||||||
|
// Clear values from chunk cookies (not the main session objects)
|
||||||
|
for k := range session.Values {
|
||||||
|
delete(session.Values, k)
|
||||||
|
}
|
||||||
|
_ = session.Save(r, w) // Best effort
|
||||||
|
}
|
||||||
|
|
||||||
|
// For main session cookies, only set expiration WITHOUT clearing values
|
||||||
|
// because sd.mainSession, sd.accessSession, etc. point to these same objects
|
||||||
|
expireLegacyMain := func(cookieName string) {
|
||||||
|
session, err := sd.manager.store.Get(r, cookieName)
|
||||||
|
if err != nil || session.IsNew {
|
||||||
|
return // Cookie doesn't exist
|
||||||
|
}
|
||||||
|
// Just expire the cookie, don't clear values (they're still needed in memory)
|
||||||
|
session.Options = &expireOptions
|
||||||
|
_ = session.Save(r, w) // Best effort
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire main legacy cookies (don't clear in-memory values)
|
||||||
|
expireLegacyMain(sd.manager.mainCookieName())
|
||||||
|
expireLegacyMain(sd.manager.accessTokenCookieName())
|
||||||
|
expireLegacyMain(sd.manager.refreshTokenCookieName())
|
||||||
|
expireLegacyMain(sd.manager.idTokenCookieName())
|
||||||
|
|
||||||
|
// Expire legacy chunk cookies (safe to clear values, they're separate from main sessions)
|
||||||
|
for i := 0; i < 50; i++ { // Max legacy chunks was 50
|
||||||
|
accessChunk := fmt.Sprintf("%s_%d", sd.manager.accessTokenCookieName(), i)
|
||||||
|
refreshChunk := fmt.Sprintf("%s_%d", sd.manager.refreshTokenCookieName(), i)
|
||||||
|
idChunk := fmt.Sprintf("%s_%d", sd.manager.idTokenCookieName(), i)
|
||||||
|
|
||||||
|
session, err := sd.manager.store.Get(r, accessChunk)
|
||||||
|
if err != nil || session.IsNew {
|
||||||
|
break // No more chunks
|
||||||
|
}
|
||||||
|
expireLegacyChunk(accessChunk)
|
||||||
|
expireLegacyChunk(refreshChunk)
|
||||||
|
expireLegacyChunk(idChunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// clearSessionValues removes all values from a session and optionally expires it.
|
// clearSessionValues removes all values from a session and optionally expires it.
|
||||||
// This is used during session cleanup and logout operations.
|
// This is used during session cleanup and logout operations.
|
||||||
// Parameters:
|
// Parameters:
|
||||||
@@ -1263,6 +1715,12 @@ func (sd *SessionData) Reset() {
|
|||||||
resetSession(sd.refreshSession)
|
resetSession(sd.refreshSession)
|
||||||
resetSession(sd.idTokenSession)
|
resetSession(sd.idTokenSession)
|
||||||
|
|
||||||
|
// Clear combined chunks
|
||||||
|
for k, session := range sd.combinedChunks {
|
||||||
|
resetSession(session)
|
||||||
|
delete(sd.combinedChunks, k)
|
||||||
|
}
|
||||||
|
|
||||||
// Clear redirect count to prevent leaking between sessions
|
// Clear redirect count to prevent leaking between sessions
|
||||||
if sd.mainSession != nil && sd.mainSession.Values != nil {
|
if sd.mainSession != nil && sd.mainSession.Values != nil {
|
||||||
delete(sd.mainSession.Values, "redirect_count")
|
delete(sd.mainSession.Values, "redirect_count")
|
||||||
@@ -1271,6 +1729,7 @@ func (sd *SessionData) Reset() {
|
|||||||
sd.dirty = false
|
sd.dirty = false
|
||||||
sd.inUse = false
|
sd.inUse = false
|
||||||
sd.request = nil
|
sd.request = nil
|
||||||
|
sd.useCombinedStorage = true // Reset to use combined storage by default
|
||||||
|
|
||||||
// Reset the refresh mutex to ensure clean state
|
// Reset the refresh mutex to ensure clean state
|
||||||
// Note: We don't need to lock it since sessionMutex is already held
|
// Note: We don't need to lock it since sessionMutex is already held
|
||||||
@@ -1886,8 +2345,9 @@ func splitIntoChunks(s string, chunkSize int) []string {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - true if the chunk is safe to store, false if it may exceed browser limits.
|
// - true if the chunk is safe to store, false if it may exceed browser limits.
|
||||||
func validateChunkSize(chunkData string) bool {
|
func validateChunkSize(chunkData string) bool {
|
||||||
|
// Estimate ~50% overhead for encoding, compare against ~2x maxCookieSize limit
|
||||||
estimatedEncodedSize := len(chunkData) + (len(chunkData) * 50 / 100)
|
estimatedEncodedSize := len(chunkData) + (len(chunkData) * 50 / 100)
|
||||||
return estimatedEncodedSize <= maxBrowserCookieSize
|
return estimatedEncodedSize <= maxCookieSize*2
|
||||||
}
|
}
|
||||||
|
|
||||||
// isCorruptionMarker detects if data contains known corruption indicators.
|
// isCorruptionMarker detects if data contains known corruption indicators.
|
||||||
@@ -2088,6 +2548,78 @@ func (sd *SessionData) getIDTokenUnsafe() string {
|
|||||||
return result.Token
|
return result.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRefreshTokenUnsafe retrieves the refresh token without acquiring locks.
|
||||||
|
// Used when the session mutex is already held to prevent deadlocks.
|
||||||
|
func (sd *SessionData) getRefreshTokenUnsafe() string {
|
||||||
|
token, _ := sd.refreshSession.Values["token"].(string)
|
||||||
|
compressed, _ := sd.refreshSession.Values["compressed"].(bool)
|
||||||
|
|
||||||
|
if sd.manager == nil || sd.manager.chunkManager == nil {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
result := sd.manager.chunkManager.GetToken(
|
||||||
|
token,
|
||||||
|
compressed,
|
||||||
|
sd.refreshTokenChunks,
|
||||||
|
RefreshTokenConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
// Handle opaque tokens
|
||||||
|
if token != "" && !compressed && len(sd.refreshTokenChunks) == 0 {
|
||||||
|
if strings.Count(token, ".") != 2 {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEmailUnsafe retrieves the email without acquiring locks.
|
||||||
|
func (sd *SessionData) getEmailUnsafe() string {
|
||||||
|
email, _ := sd.mainSession.Values["email"].(string)
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCSRFUnsafe retrieves the CSRF token without acquiring locks.
|
||||||
|
func (sd *SessionData) getCSRFUnsafe() string {
|
||||||
|
csrf, _ := sd.mainSession.Values["csrf"].(string)
|
||||||
|
return csrf
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNonceUnsafe retrieves the nonce without acquiring locks.
|
||||||
|
func (sd *SessionData) getNonceUnsafe() string {
|
||||||
|
nonce, _ := sd.mainSession.Values["nonce"].(string)
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCodeVerifierUnsafe retrieves the code verifier without acquiring locks.
|
||||||
|
func (sd *SessionData) getCodeVerifierUnsafe() string {
|
||||||
|
codeVerifier, _ := sd.mainSession.Values["code_verifier"].(string)
|
||||||
|
return codeVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIncomingPathUnsafe retrieves the incoming path without acquiring locks.
|
||||||
|
func (sd *SessionData) getIncomingPathUnsafe() string {
|
||||||
|
path, _ := sd.mainSession.Values["incoming_path"].(string)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCreatedAtUnsafe retrieves the created_at timestamp without acquiring locks.
|
||||||
|
func (sd *SessionData) getCreatedAtUnsafe() int64 {
|
||||||
|
createdAt, _ := sd.mainSession.Values["created_at"].(int64)
|
||||||
|
return createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRedirectCountUnsafe retrieves the redirect count without acquiring locks.
|
||||||
|
func (sd *SessionData) getRedirectCountUnsafe() int {
|
||||||
|
count, _ := sd.mainSession.Values["redirect_count"].(int)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
// SetIDToken stores an ID token with automatic compression and chunking.
|
// SetIDToken stores an ID token with automatic compression and chunking.
|
||||||
// It validates the JWT format, compresses if beneficial, and splits into chunks
|
// It validates the JWT format, compresses if beneficial, and splits into chunks
|
||||||
// if the token exceeds cookie size limits. Includes comprehensive validation.
|
// if the token exceeds cookie size limits. Includes comprehensive validation.
|
||||||
|
|||||||
@@ -100,13 +100,12 @@ type Logger interface {
|
|||||||
// and error handling to ensure data integrity and prevent security vulnerabilities
|
// and error handling to ensure data integrity and prevent security vulnerabilities
|
||||||
// throughout the process.
|
// throughout the process.
|
||||||
type ChunkManager struct {
|
type ChunkManager struct {
|
||||||
logger Logger
|
lastCleanup time.Time
|
||||||
mutex *sync.RWMutex
|
logger Logger
|
||||||
// sessionMap provides bounded session storage to prevent memory leaks
|
mutex *sync.RWMutex
|
||||||
sessionMap map[string]*SessionEntry
|
sessionMap map[string]*SessionEntry
|
||||||
maxSessions int
|
maxSessions int
|
||||||
sessionTTL time.Duration
|
sessionTTL time.Duration
|
||||||
lastCleanup time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChunkManager creates a new ChunkManager instance with proper initialization.
|
// NewChunkManager creates a new ChunkManager instance with proper initialization.
|
||||||
@@ -361,8 +360,8 @@ func (cm *ChunkManager) StoreSession(key string, session *sessions.Session) {
|
|||||||
if shouldEvict {
|
if shouldEvict {
|
||||||
// Find oldest sessions to remove
|
// Find oldest sessions to remove
|
||||||
type sessionAge struct {
|
type sessionAge struct {
|
||||||
key string
|
|
||||||
lastUsed time.Time
|
lastUsed time.Time
|
||||||
|
key string
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions := make([]sessionAge, 0, currentLocal)
|
sessions := make([]sessionAge, 0, currentLocal)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func TestTokenValidatorJWT(t *testing.T) {
|
|||||||
validator := NewTokenValidator()
|
validator := NewTokenValidator()
|
||||||
|
|
||||||
// Test valid JWT format (using base64url encoded parts that are long enough)
|
// Test valid JWT format (using base64url encoded parts that are long enough)
|
||||||
validJWT := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
|
validJWT := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" // trufflehog:ignore
|
||||||
err := validator.ValidateJWTFormat(validJWT, "test")
|
err := validator.ValidateJWTFormat(validJWT, "test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Expected valid JWT to pass, got error: %v", err)
|
t.Errorf("Expected valid JWT to pass, got error: %v", err)
|
||||||
@@ -186,10 +186,10 @@ func TestTokenConfigValidation(t *testing.T) {
|
|||||||
func TestSessionMapBounds_HardLimitEnforcement(t *testing.T) {
|
func TestSessionMapBounds_HardLimitEnforcement(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
description string
|
||||||
maxSessions int
|
maxSessions int
|
||||||
sessionCount int
|
sessionCount int
|
||||||
expectEviction bool
|
expectEviction bool
|
||||||
description string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "within_limit",
|
name: "within_limit",
|
||||||
@@ -760,18 +760,18 @@ func TestValidateJWTContent(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
token string
|
token string
|
||||||
expectError bool
|
|
||||||
description string
|
description string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid JWT with required ID token claims",
|
name: "Valid JWT with required ID token claims",
|
||||||
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImNsaWVudElkIiwiZXhwIjoxNjQ2MDY0MDAwLCJpYXQiOjE2NDYwNjA0MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImNsaWVudElkIiwiZXhwIjoxNjQ2MDY0MDAwLCJpYXQiOjE2NDYwNjA0MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
|
||||||
expectError: false,
|
expectError: false,
|
||||||
description: "JWT with all required ID token claims should pass",
|
description: "JWT with all required ID token claims should pass",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "JWT missing required claims",
|
name: "JWT missing required claims",
|
||||||
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
|
||||||
expectError: true,
|
expectError: true,
|
||||||
description: "JWT missing required claims should fail",
|
description: "JWT missing required claims should fail",
|
||||||
},
|
},
|
||||||
@@ -810,8 +810,8 @@ func TestValidateJWTHeader(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
header string
|
header string
|
||||||
expectError bool
|
|
||||||
description string
|
description string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid JWT header",
|
name: "Valid JWT header",
|
||||||
@@ -865,9 +865,9 @@ func TestValidateJWTPayload(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
payload string
|
payload string
|
||||||
|
description string
|
||||||
config TokenConfig
|
config TokenConfig
|
||||||
expectError bool
|
expectError bool
|
||||||
description string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid ID token payload",
|
name: "Valid ID token payload",
|
||||||
@@ -927,8 +927,8 @@ func TestValidateJWTSignature(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
signature string
|
signature string
|
||||||
expectError bool
|
|
||||||
description string
|
description string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid signature",
|
name: "Valid signature",
|
||||||
@@ -976,9 +976,9 @@ func TestValidateChunkStructure(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
description string
|
||||||
chunks []ChunkData
|
chunks []ChunkData
|
||||||
expectError bool
|
expectError bool
|
||||||
description string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid chunk structure",
|
name: "Valid chunk structure",
|
||||||
@@ -1055,11 +1055,11 @@ func TestValidateChunkData(t *testing.T) {
|
|||||||
config := AccessTokenConfig
|
config := AccessTokenConfig
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
chunk ChunkData
|
chunk ChunkData
|
||||||
|
name string
|
||||||
|
description string
|
||||||
expectedTotal int
|
expectedTotal int
|
||||||
expectError bool
|
expectError bool
|
||||||
description string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid chunk data",
|
name: "Valid chunk data",
|
||||||
@@ -1218,13 +1218,13 @@ func TestGetToken(t *testing.T) {
|
|||||||
cm := NewChunkManager(nil)
|
cm := NewChunkManager(nil)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
mainSession *sessions.Session
|
mainSession *sessions.Session
|
||||||
chunks map[int]*sessions.Session
|
chunks map[int]*sessions.Session
|
||||||
config TokenConfig
|
name string
|
||||||
expectedToken string
|
expectedToken string
|
||||||
expectError bool
|
|
||||||
description string
|
description string
|
||||||
|
config TokenConfig
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Token from main session",
|
name: "Token from main session",
|
||||||
@@ -1363,8 +1363,8 @@ func TestSerializeTokenToChunks(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
token string
|
token string
|
||||||
expectError bool
|
|
||||||
description string
|
description string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid token serialization",
|
name: "Valid token serialization",
|
||||||
@@ -1436,10 +1436,10 @@ func TestDeserializeTokenFromChunks(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
chunks []ChunkData
|
|
||||||
expectedToken string
|
expectedToken string
|
||||||
expectError bool
|
|
||||||
description string
|
description string
|
||||||
|
chunks []ChunkData
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid chunks deserialization",
|
name: "Valid chunks deserialization",
|
||||||
@@ -1522,10 +1522,10 @@ func TestEncodeDecodeChunk(t *testing.T) {
|
|||||||
cs := NewChunkSerializer(NewNoOpLogger())
|
cs := NewChunkSerializer(NewNoOpLogger())
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
chunk ChunkData
|
chunk ChunkData
|
||||||
expectError bool
|
name string
|
||||||
description string
|
description string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid chunk encoding/decoding",
|
name: "Valid chunk encoding/decoding",
|
||||||
@@ -1619,10 +1619,10 @@ func TestValidateChunkIntegrity(t *testing.T) {
|
|||||||
cs := NewChunkSerializer(NewNoOpLogger())
|
cs := NewChunkSerializer(NewNoOpLogger())
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
chunk ChunkData
|
chunk ChunkData
|
||||||
expectError bool
|
name string
|
||||||
description string
|
description string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid chunk integrity",
|
name: "Valid chunk integrity",
|
||||||
|
|||||||
@@ -254,10 +254,10 @@ func (cs *ChunkSerializer) calculateChecksum(content string) string {
|
|||||||
|
|
||||||
// ChunkData represents a single chunk of token data
|
// ChunkData represents a single chunk of token data
|
||||||
type ChunkData struct {
|
type ChunkData struct {
|
||||||
Index int // Position of this chunk in the sequence
|
Content string
|
||||||
Total int // Total number of chunks for this token
|
Checksum string
|
||||||
Content string // The actual chunk content
|
Index int
|
||||||
Checksum string // Simple checksum for integrity verification
|
Total int
|
||||||
}
|
}
|
||||||
|
|
||||||
// EstimateChunkCount estimates how many chunks a token will need
|
// EstimateChunkCount estimates how many chunks a token will need
|
||||||
|
|||||||
+10
-11
@@ -84,21 +84,19 @@ type TokenRetrievalResult struct {
|
|||||||
// and error handling to ensure data integrity and prevent security vulnerabilities
|
// and error handling to ensure data integrity and prevent security vulnerabilities
|
||||||
// throughout the process.
|
// throughout the process.
|
||||||
type ChunkManager struct {
|
type ChunkManager struct {
|
||||||
logger *Logger
|
lastCleanup time.Time
|
||||||
mutex *sync.RWMutex
|
ctx context.Context
|
||||||
ctx context.Context
|
mutex *sync.RWMutex
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
wg sync.WaitGroup // WaitGroup to track background goroutine completion
|
|
||||||
// sessionMap provides bounded session storage to prevent memory leaks
|
|
||||||
sessionMap map[string]*SessionEntry
|
sessionMap map[string]*SessionEntry
|
||||||
|
logger *Logger
|
||||||
|
wg sync.WaitGroup
|
||||||
maxSessions int
|
maxSessions int
|
||||||
sessionTTL time.Duration
|
sessionTTL time.Duration
|
||||||
lastCleanup time.Time
|
|
||||||
cleanupRunning int32 // atomic flag to prevent concurrent cleanups
|
|
||||||
// Memory usage tracking
|
|
||||||
bytesAllocated int64
|
bytesAllocated int64
|
||||||
peakSessions int64
|
peakSessions int64
|
||||||
cleanupCount int64
|
cleanupCount int64
|
||||||
|
cleanupRunning int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionEntry represents a session with expiration tracking
|
// SessionEntry represents a session with expiration tracking
|
||||||
@@ -393,7 +391,8 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
|
|||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(chunk) > maxBrowserCookieSize {
|
// Secondary check: ensure chunk won't exceed browser limit after encoding (~2x overhead)
|
||||||
|
if len(chunk) > maxCookieSize*2 {
|
||||||
err := fmt.Errorf("%s token chunk %d exceeds browser limit (%d bytes)",
|
err := fmt.Errorf("%s token chunk %d exceeds browser limit (%d bytes)",
|
||||||
config.Type, i, len(chunk))
|
config.Type, i, len(chunk))
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
@@ -1199,8 +1198,8 @@ func (cm *ChunkManager) findOldestSessions(k int) []string {
|
|||||||
|
|
||||||
// Collect all timestamps with keys
|
// Collect all timestamps with keys
|
||||||
type sessionAge struct {
|
type sessionAge struct {
|
||||||
key string
|
|
||||||
lastUsed time.Time
|
lastUsed time.Time
|
||||||
|
key string
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions := make([]sessionAge, 0, len(cm.sessionMap))
|
sessions := make([]sessionAge, 0, len(cm.sessionMap))
|
||||||
|
|||||||
+15
-15
@@ -24,31 +24,31 @@ import (
|
|||||||
|
|
||||||
// SessionTestCase represents a comprehensive session test scenario
|
// SessionTestCase represents a comprehensive session test scenario
|
||||||
type SessionTestCase struct {
|
type SessionTestCase struct {
|
||||||
name string
|
|
||||||
scenario string // "creation", "validation", "expiration", "persistence", "cleanup", "chunking", "security"
|
|
||||||
sessionType string // "user", "admin", "api", "guest", "csrf"
|
|
||||||
setup func(*SessionTestFramework)
|
setup func(*SessionTestFramework)
|
||||||
execute func(*SessionTestFramework) error
|
execute func(*SessionTestFramework) error
|
||||||
validate func(*testing.T, error, *SessionTestFramework)
|
validate func(*testing.T, error, *SessionTestFramework)
|
||||||
cleanup func(*SessionTestFramework)
|
cleanup func(*SessionTestFramework)
|
||||||
concurrent bool
|
name string
|
||||||
|
scenario string
|
||||||
|
sessionType string
|
||||||
|
skipReason string
|
||||||
iterations int
|
iterations int
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
skipReason string
|
concurrent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionTestFramework provides shared test infrastructure for session tests
|
// SessionTestFramework provides shared test infrastructure for session tests
|
||||||
type SessionTestFramework struct {
|
type SessionTestFramework struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
mockProvider *httptest.Server
|
mockProvider *httptest.Server
|
||||||
|
testTokens map[string]string
|
||||||
|
metrics *SessionTestMetrics
|
||||||
|
config *SessionTestConfig
|
||||||
requests []*http.Request
|
requests []*http.Request
|
||||||
responses []*httptest.ResponseRecorder
|
responses []*httptest.ResponseRecorder
|
||||||
testTokens map[string]string
|
|
||||||
sessionIDs []string
|
sessionIDs []string
|
||||||
mu sync.RWMutex
|
|
||||||
metrics *SessionTestMetrics
|
|
||||||
cleanupFuncs []func()
|
cleanupFuncs []func()
|
||||||
config *SessionTestConfig
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionTestMetrics tracks test performance metrics
|
// SessionTestMetrics tracks test performance metrics
|
||||||
@@ -65,12 +65,12 @@ type SessionTestMetrics struct {
|
|||||||
|
|
||||||
// SessionTestConfig holds test configuration
|
// SessionTestConfig holds test configuration
|
||||||
type SessionTestConfig struct {
|
type SessionTestConfig struct {
|
||||||
|
CookieDomain string
|
||||||
|
EncryptionKey string
|
||||||
MaxChunkSize int
|
MaxChunkSize int
|
||||||
MaxSessions int
|
MaxSessions int
|
||||||
EnableHTTPS bool
|
|
||||||
CookieDomain string
|
|
||||||
SessionTimeout time.Duration
|
SessionTimeout time.Duration
|
||||||
EncryptionKey string
|
EnableHTTPS bool
|
||||||
EnableCompression bool
|
EnableCompression bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2849,9 +2849,9 @@ func TestSessionExpiryVsTokenExpiry(t *testing.T) {
|
|||||||
|
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
|
expectedBehavior string
|
||||||
sessionAge time.Duration
|
sessionAge time.Duration
|
||||||
tokenExpiry time.Duration
|
tokenExpiry time.Duration
|
||||||
expectedBehavior string
|
|
||||||
sessionShouldExpire bool
|
sessionShouldExpire bool
|
||||||
tokenShouldRefresh bool
|
tokenShouldRefresh bool
|
||||||
}{
|
}{
|
||||||
@@ -2975,10 +2975,10 @@ func TestSessionCleanupOnTokenExpiry(t *testing.T) {
|
|||||||
|
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
tokenExpiry time.Duration
|
|
||||||
shouldCleanup bool
|
|
||||||
shouldPreserve []string
|
shouldPreserve []string
|
||||||
shouldRemove []string
|
shouldRemove []string
|
||||||
|
tokenExpiry time.Duration
|
||||||
|
shouldCleanup bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Recently expired tokens - preserve session",
|
name: "Recently expired tokens - preserve session",
|
||||||
|
|||||||
+103
-334
@@ -27,359 +27,128 @@ type TemplatedHeader struct {
|
|||||||
// It provides all necessary settings to configure OpenID Connect authentication
|
// It provides all necessary settings to configure OpenID Connect authentication
|
||||||
// with various providers like Auth0, Logto, or any standard OIDC provider.
|
// with various providers like Auth0, Logto, or any standard OIDC provider.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
HTTPClient *http.Client `json:"-"`
|
|
||||||
OIDCEndSessionURL string `json:"oidcEndSessionURL"`
|
|
||||||
CookieDomain string `json:"cookieDomain"`
|
|
||||||
CookiePrefix string `json:"cookiePrefix"` // Prefix for session cookie names (default: "_oidc_raczylo_")
|
|
||||||
SessionMaxAge int `json:"sessionMaxAge"` // Maximum session age in seconds (default: 86400 = 24 hours)
|
|
||||||
CallbackURL string `json:"callbackURL"`
|
|
||||||
LogoutURL string `json:"logoutURL"`
|
|
||||||
ClientID string `json:"clientID"`
|
|
||||||
ClientSecret string `json:"clientSecret"`
|
|
||||||
// Audience specifies the expected JWT audience claim value.
|
|
||||||
// If not set, defaults to ClientID for backward compatibility.
|
|
||||||
// For Auth0 API access tokens with custom audiences, set this to your API identifier.
|
|
||||||
// For Azure AD with Application ID URI, set to "api://your-app-id".
|
|
||||||
// Security: This value is validated against the JWT aud claim to prevent token confusion attacks.
|
|
||||||
Audience string `json:"audience,omitempty"`
|
|
||||||
PostLogoutRedirectURI string `json:"postLogoutRedirectURI"`
|
|
||||||
LogLevel string `json:"logLevel"`
|
|
||||||
SessionEncryptionKey string `json:"sessionEncryptionKey"`
|
|
||||||
ProviderURL string `json:"providerURL"`
|
|
||||||
RevocationURL string `json:"revocationURL"`
|
|
||||||
ExcludedURLs []string `json:"excludedURLs"`
|
|
||||||
AllowedUserDomains []string `json:"allowedUserDomains"`
|
|
||||||
AllowedUsers []string `json:"allowedUsers"`
|
|
||||||
Scopes []string `json:"scopes"`
|
|
||||||
Headers []TemplatedHeader `json:"headers"`
|
|
||||||
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
|
|
||||||
RateLimit int `json:"rateLimit"`
|
|
||||||
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
|
||||||
ForceHTTPS bool `json:"forceHTTPS"`
|
|
||||||
EnablePKCE bool `json:"enablePKCE"`
|
|
||||||
OverrideScopes bool `json:"overrideScopes"`
|
|
||||||
// StrictAudienceValidation enforces strict audience validation for access tokens.
|
|
||||||
// When enabled, sessions are rejected if access token validation fails (prevents fallback to ID token).
|
|
||||||
// This addresses Auth0 Scenario 2 security concerns where access tokens without proper
|
|
||||||
// audience claims could be accepted based on ID token validation.
|
|
||||||
// Default: false (backward compatible - allows ID token fallback)
|
|
||||||
// Recommended: true for production environments requiring strict OAuth 2.0 compliance
|
|
||||||
StrictAudienceValidation bool `json:"strictAudienceValidation,omitempty"`
|
|
||||||
// AllowOpaqueTokens enables acceptance of non-JWT (opaque) access tokens.
|
|
||||||
// When enabled, opaque tokens are validated via OAuth 2.0 Token Introspection (RFC 7662).
|
|
||||||
// This supports Auth0 Scenario 3 and other providers that issue opaque access tokens.
|
|
||||||
// Default: false (only JWT access tokens accepted)
|
|
||||||
// Note: Requires introspection endpoint to be available from provider metadata
|
|
||||||
AllowOpaqueTokens bool `json:"allowOpaqueTokens,omitempty"`
|
|
||||||
// RequireTokenIntrospection forces token introspection for all opaque access tokens.
|
|
||||||
// When enabled, opaque tokens are rejected if introspection endpoint is unavailable.
|
|
||||||
// When disabled, opaque tokens fall back to ID token validation.
|
|
||||||
// Default: false (allows fallback to ID token)
|
|
||||||
// Recommended: true when AllowOpaqueTokens is enabled for maximum security
|
|
||||||
RequireTokenIntrospection bool `json:"requireTokenIntrospection,omitempty"`
|
|
||||||
// DisableReplayDetection disables JTI-based replay attack detection.
|
|
||||||
// Enable this when running multiple Traefik replicas to prevent false positives.
|
|
||||||
// Each replica maintains its own in-memory JTI cache, so the same valid token
|
|
||||||
// hitting different replicas will trigger replay detection on subsequent requests.
|
|
||||||
//
|
|
||||||
// Security Note: When enabled, the plugin still validates token signatures,
|
|
||||||
// expiration, and other claims. Only the JTI replay check is disabled.
|
|
||||||
// Consider using a shared cache backend (Redis/Memcached) if replay detection
|
|
||||||
// is required in multi-replica scenarios.
|
|
||||||
//
|
|
||||||
// Default: false (replay detection enabled)
|
|
||||||
// Recommended: true for multi-replica deployments
|
|
||||||
DisableReplayDetection bool `json:"disableReplayDetection,omitempty"`
|
|
||||||
SecurityHeaders *SecurityHeadersConfig `json:"securityHeaders,omitempty"`
|
|
||||||
|
|
||||||
// Redis configures the Redis cache backend for distributed caching.
|
|
||||||
// When enabled, provides cache sharing across multiple Traefik replicas.
|
|
||||||
// Default: nil (disabled - uses in-memory caching)
|
|
||||||
Redis *RedisConfig `json:"redis,omitempty"`
|
|
||||||
|
|
||||||
// RoleClaimName specifies the JWT claim name to extract user roles from.
|
|
||||||
// This allows compatibility with different OIDC providers that use different claim names.
|
|
||||||
//
|
|
||||||
// Examples:
|
|
||||||
// - Default (backward compatible): "roles"
|
|
||||||
// - Auth0 namespaced: "https://myapp.com/roles"
|
|
||||||
// - Keycloak realm roles: "realm_access.roles"
|
|
||||||
// - Custom claim: "user_roles"
|
|
||||||
//
|
|
||||||
// If not specified, defaults to "roles" for backward compatibility.
|
|
||||||
// Supports both simple names and namespaced URIs per OIDC specification.
|
|
||||||
//
|
|
||||||
// Default: "roles"
|
|
||||||
RoleClaimName string `json:"roleClaimName,omitempty"`
|
|
||||||
|
|
||||||
// GroupClaimName specifies the JWT claim name to extract user groups from.
|
|
||||||
// This allows compatibility with different OIDC providers that use different claim names.
|
|
||||||
//
|
|
||||||
// Examples:
|
|
||||||
// - Default (backward compatible): "groups"
|
|
||||||
// - Auth0 namespaced: "https://myapp.com/groups"
|
|
||||||
// - Azure AD groups: "groups"
|
|
||||||
// - Custom claim: "user_groups"
|
|
||||||
//
|
|
||||||
// If not specified, defaults to "groups" for backward compatibility.
|
|
||||||
// Supports both simple names and namespaced URIs per OIDC specification.
|
|
||||||
//
|
|
||||||
// Default: "groups"
|
|
||||||
GroupClaimName string `json:"groupClaimName,omitempty"`
|
|
||||||
|
|
||||||
// UserIdentifierClaim specifies the JWT claim to use as the user identifier.
|
|
||||||
// This allows authentication for users without email addresses (e.g., Azure AD service accounts).
|
|
||||||
//
|
|
||||||
// Examples:
|
|
||||||
// - Default (backward compatible): "email"
|
|
||||||
// - Azure AD without email: "sub", "oid", "upn", or "preferred_username"
|
|
||||||
// - Generic OIDC: "sub" (always present per OIDC spec)
|
|
||||||
//
|
|
||||||
// When set to a non-email claim:
|
|
||||||
// - AllowedUsers will match against this claim value instead of email
|
|
||||||
// - AllowedUserDomains validation is skipped (domains only apply to email)
|
|
||||||
// - The session will store this identifier as the user's identity
|
|
||||||
//
|
|
||||||
// Default: "email"
|
|
||||||
UserIdentifierClaim string `json:"userIdentifierClaim,omitempty"`
|
|
||||||
|
|
||||||
// DynamicClientRegistration enables OIDC Dynamic Client Registration (RFC 7591)
|
|
||||||
// When enabled, the middleware will automatically register as a client with
|
|
||||||
// the OIDC provider if ClientID/ClientSecret are not provided.
|
|
||||||
DynamicClientRegistration *DynamicClientRegistrationConfig `json:"dynamicClientRegistration,omitempty"`
|
DynamicClientRegistration *DynamicClientRegistrationConfig `json:"dynamicClientRegistration,omitempty"`
|
||||||
|
Redis *RedisConfig `json:"redis,omitempty"`
|
||||||
// AllowPrivateIPAddresses disables the security check that blocks private/internal IP addresses.
|
HTTPClient *http.Client `json:"-"`
|
||||||
// By default, the plugin rejects URLs containing private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
|
SecurityHeaders *SecurityHeadersConfig `json:"securityHeaders,omitempty"`
|
||||||
// to prevent SSRF attacks and ensure OIDC providers are publicly accessible.
|
PostLogoutRedirectURI string `json:"postLogoutRedirectURI"`
|
||||||
//
|
LogLevel string `json:"logLevel"`
|
||||||
// Enable this option ONLY when:
|
LogoutURL string `json:"logoutURL"`
|
||||||
// - Your OIDC provider (e.g., Keycloak) runs on an internal network with private IPs
|
ClientID string `json:"clientID"`
|
||||||
// - You have no DNS resolution available for internal services
|
ClientSecret string `json:"clientSecret"`
|
||||||
// - Your entire stack runs in a Docker network or Kubernetes cluster with private addressing
|
Audience string `json:"audience,omitempty"`
|
||||||
//
|
CookiePrefix string `json:"cookiePrefix"`
|
||||||
// Security Warning: Enabling this option reduces SSRF protection. Only use in trusted
|
CallbackURL string `json:"callbackURL"`
|
||||||
// network environments where the OIDC provider is known and controlled.
|
SessionEncryptionKey string `json:"sessionEncryptionKey"`
|
||||||
//
|
ProviderURL string `json:"providerURL"`
|
||||||
// Default: false (private IPs are blocked for security)
|
RevocationURL string `json:"revocationURL"`
|
||||||
AllowPrivateIPAddresses bool `json:"allowPrivateIPAddresses,omitempty"`
|
UserIdentifierClaim string `json:"userIdentifierClaim,omitempty"`
|
||||||
|
GroupClaimName string `json:"groupClaimName,omitempty"`
|
||||||
// MinimalHeaders reduces the number of headers forwarded to downstream services.
|
RoleClaimName string `json:"roleClaimName,omitempty"`
|
||||||
// This helps prevent "431 Request Header Fields Too Large" errors when downstream
|
CookieDomain string `json:"cookieDomain"`
|
||||||
// services have limited header buffer sizes.
|
OIDCEndSessionURL string `json:"oidcEndSessionURL"`
|
||||||
//
|
Scopes []string `json:"scopes"`
|
||||||
// When enabled (true):
|
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
|
||||||
// - Only forwards: X-Forwarded-User
|
ExcludedURLs []string `json:"excludedURLs"`
|
||||||
// - Skips: X-Auth-Request-Token (full ID token), X-Auth-Request-Redirect
|
AllowedUserDomains []string `json:"allowedUserDomains"`
|
||||||
// - Groups/roles headers (X-User-Groups, X-User-Roles) are still forwarded if configured
|
AllowedUsers []string `json:"allowedUsers"`
|
||||||
// - Custom templated headers are still processed
|
Headers []TemplatedHeader `json:"headers"`
|
||||||
//
|
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
||||||
// When disabled (false, default):
|
SessionMaxAge int `json:"sessionMaxAge"`
|
||||||
// - Forwards all headers: X-Forwarded-User, X-Auth-Request-User, X-Auth-Request-Redirect,
|
RateLimit int `json:"rateLimit"`
|
||||||
// X-Auth-Request-Token (full ID token)
|
OverrideScopes bool `json:"overrideScopes"`
|
||||||
//
|
DisableReplayDetection bool `json:"disableReplayDetection,omitempty"`
|
||||||
// Use this option when:
|
RequireTokenIntrospection bool `json:"requireTokenIntrospection,omitempty"`
|
||||||
// - Downstream services return "431 Request Header Fields Too Large" errors
|
AllowOpaqueTokens bool `json:"allowOpaqueTokens,omitempty"`
|
||||||
// - You don't need the full ID token forwarded to backend services
|
StrictAudienceValidation bool `json:"strictAudienceValidation,omitempty"`
|
||||||
// - You want to reduce request overhead
|
EnablePKCE bool `json:"enablePKCE"`
|
||||||
//
|
ForceHTTPS bool `json:"forceHTTPS"`
|
||||||
// Default: false (all headers forwarded for backward compatibility)
|
AllowPrivateIPAddresses bool `json:"allowPrivateIPAddresses,omitempty"`
|
||||||
MinimalHeaders bool `json:"minimalHeaders,omitempty"`
|
MinimalHeaders bool `json:"minimalHeaders,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedisConfig configures Redis cache backend settings for distributed caching.
|
// RedisConfig configures Redis cache backend settings for distributed caching.
|
||||||
// All fields support both JSON and YAML configuration for compatibility with Traefik's
|
// All fields support both JSON and YAML configuration for compatibility with Traefik's
|
||||||
// dynamic configuration (labels, YAML files, etc.)
|
// dynamic configuration (labels, YAML files, etc.)
|
||||||
type RedisConfig struct {
|
type RedisConfig struct {
|
||||||
// Enabled indicates if Redis caching should be used (default: false)
|
KeyPrefix string `json:"keyPrefix" yaml:"keyPrefix"`
|
||||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
Address string `json:"address" yaml:"address"`
|
||||||
|
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
||||||
// Address is the Redis server address (e.g., "localhost:6379", "redis:6379")
|
CacheMode string `json:"cacheMode" yaml:"cacheMode"`
|
||||||
Address string `json:"address" yaml:"address"`
|
WriteTimeout int `json:"writeTimeout" yaml:"writeTimeout"`
|
||||||
|
CircuitBreakerThreshold int `json:"circuitBreakerThreshold" yaml:"circuitBreakerThreshold"`
|
||||||
// Password for Redis authentication (optional, leave empty for no auth)
|
ConnectTimeout int `json:"connectTimeout" yaml:"connectTimeout"`
|
||||||
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
ReadTimeout int `json:"readTimeout" yaml:"readTimeout"`
|
||||||
|
PoolSize int `json:"poolSize" yaml:"poolSize"`
|
||||||
// DB is the Redis database number to use (default: 0)
|
HealthCheckInterval int `json:"healthCheckInterval" yaml:"healthCheckInterval"`
|
||||||
DB int `json:"db" yaml:"db"`
|
CircuitBreakerTimeout int `json:"circuitBreakerTimeout" yaml:"circuitBreakerTimeout"`
|
||||||
|
DB int `json:"db" yaml:"db"`
|
||||||
// KeyPrefix is the prefix for all Redis keys (default: "traefikoidc:")
|
HybridL1Size int `json:"hybridL1Size" yaml:"hybridL1Size"`
|
||||||
KeyPrefix string `json:"keyPrefix" yaml:"keyPrefix"`
|
HybridL1MemoryMB int64 `json:"hybridL1MemoryMB" yaml:"hybridL1MemoryMB"`
|
||||||
|
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||||
// PoolSize is the maximum number of socket connections (default: 10)
|
EnableCircuitBreaker bool `json:"enableCircuitBreaker" yaml:"enableCircuitBreaker"`
|
||||||
PoolSize int `json:"poolSize" yaml:"poolSize"`
|
TLSSkipVerify bool `json:"tlsSkipVerify" yaml:"tlsSkipVerify"`
|
||||||
|
EnableHealthCheck bool `json:"enableHealthCheck" yaml:"enableHealthCheck"`
|
||||||
// ConnectTimeout is the timeout for establishing connections in seconds (default: 5)
|
EnableTLS bool `json:"enableTLS" yaml:"enableTLS"`
|
||||||
ConnectTimeout int `json:"connectTimeout" yaml:"connectTimeout"`
|
|
||||||
|
|
||||||
// ReadTimeout is the timeout for read operations in seconds (default: 3)
|
|
||||||
ReadTimeout int `json:"readTimeout" yaml:"readTimeout"`
|
|
||||||
|
|
||||||
// WriteTimeout is the timeout for write operations in seconds (default: 3)
|
|
||||||
WriteTimeout int `json:"writeTimeout" yaml:"writeTimeout"`
|
|
||||||
|
|
||||||
// EnableTLS indicates if TLS should be used for Redis connections (default: false)
|
|
||||||
EnableTLS bool `json:"enableTLS" yaml:"enableTLS"`
|
|
||||||
|
|
||||||
// TLSSkipVerify skips TLS certificate verification (not recommended for production)
|
|
||||||
TLSSkipVerify bool `json:"tlsSkipVerify" yaml:"tlsSkipVerify"`
|
|
||||||
|
|
||||||
// CacheMode determines the caching strategy: "redis" (Redis only), "hybrid" (Memory+Redis), "memory" (Memory only)
|
|
||||||
// Default: "redis" when enabled
|
|
||||||
CacheMode string `json:"cacheMode" yaml:"cacheMode"`
|
|
||||||
|
|
||||||
// HybridL1Size is the maximum number of items in L1 cache for hybrid mode (default: 500)
|
|
||||||
HybridL1Size int `json:"hybridL1Size" yaml:"hybridL1Size"`
|
|
||||||
|
|
||||||
// HybridL1MemoryMB is the maximum memory in MB for L1 cache in hybrid mode (default: 10)
|
|
||||||
HybridL1MemoryMB int64 `json:"hybridL1MemoryMB" yaml:"hybridL1MemoryMB"`
|
|
||||||
|
|
||||||
// EnableCircuitBreaker enables circuit breaker for Redis failures (default: true)
|
|
||||||
EnableCircuitBreaker bool `json:"enableCircuitBreaker" yaml:"enableCircuitBreaker"`
|
|
||||||
|
|
||||||
// CircuitBreakerThreshold is the number of failures before opening circuit (default: 5)
|
|
||||||
CircuitBreakerThreshold int `json:"circuitBreakerThreshold" yaml:"circuitBreakerThreshold"`
|
|
||||||
|
|
||||||
// CircuitBreakerTimeout is the timeout in seconds before attempting to close circuit (default: 60)
|
|
||||||
CircuitBreakerTimeout int `json:"circuitBreakerTimeout" yaml:"circuitBreakerTimeout"`
|
|
||||||
|
|
||||||
// EnableHealthCheck enables periodic health checks for Redis (default: true)
|
|
||||||
EnableHealthCheck bool `json:"enableHealthCheck" yaml:"enableHealthCheck"`
|
|
||||||
|
|
||||||
// HealthCheckInterval is the interval in seconds between health checks (default: 30)
|
|
||||||
HealthCheckInterval int `json:"healthCheckInterval" yaml:"healthCheckInterval"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DynamicClientRegistrationConfig configures OIDC Dynamic Client Registration (RFC 7591)
|
// DynamicClientRegistrationConfig configures OIDC Dynamic Client Registration (RFC 7591)
|
||||||
type DynamicClientRegistrationConfig struct {
|
type DynamicClientRegistrationConfig struct {
|
||||||
// Enabled enables automatic client registration with the OIDC provider
|
ClientMetadata *ClientRegistrationMetadata `json:"clientMetadata,omitempty"`
|
||||||
Enabled bool `json:"enabled"`
|
InitialAccessToken string `json:"initialAccessToken,omitempty"`
|
||||||
|
RegistrationEndpoint string `json:"registrationEndpoint,omitempty"`
|
||||||
// InitialAccessToken is an optional bearer token for protected registration endpoints
|
CredentialsFile string `json:"credentialsFile,omitempty"`
|
||||||
// Some providers require this token to authorize new client registrations
|
Enabled bool `json:"enabled"`
|
||||||
InitialAccessToken string `json:"initialAccessToken,omitempty"`
|
PersistCredentials bool `json:"persistCredentials"`
|
||||||
|
|
||||||
// RegistrationEndpoint overrides the endpoint discovered from provider metadata
|
|
||||||
// If empty, uses the registration_endpoint from .well-known/openid-configuration
|
|
||||||
RegistrationEndpoint string `json:"registrationEndpoint,omitempty"`
|
|
||||||
|
|
||||||
// ClientMetadata contains the client metadata to register
|
|
||||||
ClientMetadata *ClientRegistrationMetadata `json:"clientMetadata,omitempty"`
|
|
||||||
|
|
||||||
// PersistCredentials determines whether to save registered credentials to a file
|
|
||||||
// This allows reusing the same client_id/client_secret across restarts
|
|
||||||
PersistCredentials bool `json:"persistCredentials"`
|
|
||||||
|
|
||||||
// CredentialsFile is the path to store/load registered client credentials
|
|
||||||
// Defaults to "/tmp/oidc-client-credentials.json" if not specified
|
|
||||||
CredentialsFile string `json:"credentialsFile,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientRegistrationMetadata contains client metadata for dynamic registration (RFC 7591)
|
// ClientRegistrationMetadata contains client metadata for dynamic registration (RFC 7591)
|
||||||
type ClientRegistrationMetadata struct {
|
type ClientRegistrationMetadata struct {
|
||||||
// RedirectURIs is REQUIRED - array of redirect URIs for authorization
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
||||||
RedirectURIs []string `json:"redirect_uris"`
|
TOSURI string `json:"tos_uri,omitempty"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
// ResponseTypes specifies OAuth 2.0 response types (default: ["code"])
|
ApplicationType string `json:"application_type,omitempty"`
|
||||||
ResponseTypes []string `json:"response_types,omitempty"`
|
SubjectType string `json:"subject_type,omitempty"`
|
||||||
|
ClientName string `json:"client_name,omitempty"`
|
||||||
// GrantTypes specifies OAuth 2.0 grant types (default: ["authorization_code"])
|
LogoURI string `json:"logo_uri,omitempty"`
|
||||||
GrantTypes []string `json:"grant_types,omitempty"`
|
ClientURI string `json:"client_uri,omitempty"`
|
||||||
|
PolicyURI string `json:"policy_uri,omitempty"`
|
||||||
// ApplicationType is either "web" (default) or "native"
|
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||||
ApplicationType string `json:"application_type,omitempty"`
|
ResponseTypes []string `json:"response_types,omitempty"`
|
||||||
|
Contacts []string `json:"contacts,omitempty"`
|
||||||
// Contacts is an array of email addresses for responsible parties
|
RedirectURIs []string `json:"redirect_uris"`
|
||||||
Contacts []string `json:"contacts,omitempty"`
|
DefaultACRValues []string `json:"default_acr_values,omitempty"`
|
||||||
|
GrantTypes []string `json:"grant_types,omitempty"`
|
||||||
// ClientName is a human-readable name for the client
|
DefaultMaxAge int `json:"default_max_age,omitempty"`
|
||||||
ClientName string `json:"client_name,omitempty"`
|
RequireAuthTime bool `json:"require_auth_time,omitempty"`
|
||||||
|
|
||||||
// LogoURI is a URL pointing to a logo for the client
|
|
||||||
LogoURI string `json:"logo_uri,omitempty"`
|
|
||||||
|
|
||||||
// ClientURI is a URL of the home page of the client
|
|
||||||
ClientURI string `json:"client_uri,omitempty"`
|
|
||||||
|
|
||||||
// PolicyURI is a URL pointing to the client's privacy policy
|
|
||||||
PolicyURI string `json:"policy_uri,omitempty"`
|
|
||||||
|
|
||||||
// TOSURI is a URL pointing to the client's terms of service
|
|
||||||
TOSURI string `json:"tos_uri,omitempty"`
|
|
||||||
|
|
||||||
// JWKSURI is a URL for the client's JSON Web Key Set
|
|
||||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
|
||||||
|
|
||||||
// SubjectType is "pairwise" or "public" (provider-specific)
|
|
||||||
SubjectType string `json:"subject_type,omitempty"`
|
|
||||||
|
|
||||||
// TokenEndpointAuthMethod specifies how the client authenticates at token endpoint
|
|
||||||
// Values: "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"
|
|
||||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
|
||||||
|
|
||||||
// DefaultMaxAge is the default maximum authentication age in seconds
|
|
||||||
DefaultMaxAge int `json:"default_max_age,omitempty"`
|
|
||||||
|
|
||||||
// RequireAuthTime specifies whether auth_time claim is required in ID token
|
|
||||||
RequireAuthTime bool `json:"require_auth_time,omitempty"`
|
|
||||||
|
|
||||||
// DefaultACRValues specifies default ACR values
|
|
||||||
DefaultACRValues []string `json:"default_acr_values,omitempty"`
|
|
||||||
|
|
||||||
// Scope is a space-separated list of scopes (alternative to config.Scopes)
|
|
||||||
Scope string `json:"scope,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecurityHeadersConfig configures security headers for the plugin
|
// SecurityHeadersConfig configures security headers for the plugin
|
||||||
type SecurityHeadersConfig struct {
|
type SecurityHeadersConfig struct {
|
||||||
// Enable security headers (default: true)
|
CustomHeaders map[string]string `json:"customHeaders,omitempty"`
|
||||||
Enabled bool `json:"enabled"`
|
PermissionsPolicy string `json:"permissionsPolicy,omitempty"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
// Security profile: "default", "strict", "development", "api", or "custom"
|
ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty"`
|
||||||
Profile string `json:"profile"`
|
CrossOriginResourcePolicy string `json:"crossOriginResourcePolicy,omitempty"`
|
||||||
|
CrossOriginOpenerPolicy string `json:"crossOriginOpenerPolicy,omitempty"`
|
||||||
// Content Security Policy
|
CrossOriginEmbedderPolicy string `json:"crossOriginEmbedderPolicy,omitempty"`
|
||||||
ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty"`
|
FrameOptions string `json:"frameOptions,omitempty"`
|
||||||
|
ContentTypeOptions string `json:"contentTypeOptions,omitempty"`
|
||||||
// HSTS settings
|
XSSProtection string `json:"xssProtection,omitempty"`
|
||||||
StrictTransportSecurity bool `json:"strictTransportSecurity"`
|
ReferrerPolicy string `json:"referrerPolicy,omitempty"`
|
||||||
StrictTransportSecurityMaxAge int `json:"strictTransportSecurityMaxAge"` // seconds
|
CORSAllowedHeaders []string `json:"corsAllowedHeaders,omitempty"`
|
||||||
StrictTransportSecuritySubdomains bool `json:"strictTransportSecuritySubdomains"`
|
CORSAllowedOrigins []string `json:"corsAllowedOrigins,omitempty"`
|
||||||
StrictTransportSecurityPreload bool `json:"strictTransportSecurityPreload"`
|
CORSAllowedMethods []string `json:"corsAllowedMethods,omitempty"`
|
||||||
|
StrictTransportSecurityMaxAge int `json:"strictTransportSecurityMaxAge"`
|
||||||
// Frame options: "DENY", "SAMEORIGIN", or "ALLOW-FROM uri"
|
CORSMaxAge int `json:"corsMaxAge"`
|
||||||
FrameOptions string `json:"frameOptions,omitempty"`
|
StrictTransportSecurityPreload bool `json:"strictTransportSecurityPreload"`
|
||||||
|
StrictTransportSecuritySubdomains bool `json:"strictTransportSecuritySubdomains"`
|
||||||
// Content type options (default: "nosniff")
|
CORSEnabled bool `json:"corsEnabled"`
|
||||||
ContentTypeOptions string `json:"contentTypeOptions,omitempty"`
|
Enabled bool `json:"enabled"`
|
||||||
|
CORSAllowCredentials bool `json:"corsAllowCredentials"`
|
||||||
// XSS protection (default: "1; mode=block")
|
StrictTransportSecurity bool `json:"strictTransportSecurity"`
|
||||||
XSSProtection string `json:"xssProtection,omitempty"`
|
DisableServerHeader bool `json:"disableServerHeader"`
|
||||||
|
DisablePoweredByHeader bool `json:"disablePoweredByHeader"`
|
||||||
// Referrer policy
|
|
||||||
ReferrerPolicy string `json:"referrerPolicy,omitempty"`
|
|
||||||
|
|
||||||
// Permissions policy
|
|
||||||
PermissionsPolicy string `json:"permissionsPolicy,omitempty"`
|
|
||||||
|
|
||||||
// Cross-origin settings
|
|
||||||
CrossOriginEmbedderPolicy string `json:"crossOriginEmbedderPolicy,omitempty"`
|
|
||||||
CrossOriginOpenerPolicy string `json:"crossOriginOpenerPolicy,omitempty"`
|
|
||||||
CrossOriginResourcePolicy string `json:"crossOriginResourcePolicy,omitempty"`
|
|
||||||
|
|
||||||
// CORS settings
|
|
||||||
CORSEnabled bool `json:"corsEnabled"`
|
|
||||||
CORSAllowedOrigins []string `json:"corsAllowedOrigins,omitempty"`
|
|
||||||
CORSAllowedMethods []string `json:"corsAllowedMethods,omitempty"`
|
|
||||||
CORSAllowedHeaders []string `json:"corsAllowedHeaders,omitempty"`
|
|
||||||
CORSAllowCredentials bool `json:"corsAllowCredentials"`
|
|
||||||
CORSMaxAge int `json:"corsMaxAge"` // seconds
|
|
||||||
|
|
||||||
// Custom headers (in addition to standard security headers)
|
|
||||||
CustomHeaders map[string]string `json:"customHeaders,omitempty"`
|
|
||||||
|
|
||||||
// Security features
|
|
||||||
DisableServerHeader bool `json:"disableServerHeader"`
|
|
||||||
DisablePoweredByHeader bool `json:"disablePoweredByHeader"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+1
-1
@@ -17,8 +17,8 @@ type ShardedCache struct {
|
|||||||
|
|
||||||
// cacheShard represents a single shard with its own mutex and data map.
|
// cacheShard represents a single shard with its own mutex and data map.
|
||||||
type cacheShard struct {
|
type cacheShard struct {
|
||||||
mu sync.RWMutex
|
|
||||||
items map[string]*shardedCacheItem
|
items map[string]*shardedCacheItem
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// shardedCacheItem represents an item in the sharded cache with expiration.
|
// shardedCacheItem represents an item in the sharded cache with expiration.
|
||||||
|
|||||||
+18
-33
@@ -18,33 +18,20 @@ var (
|
|||||||
// ResourceManager manages shared resources across all middleware instances
|
// ResourceManager manages shared resources across all middleware instances
|
||||||
// to prevent duplication and goroutine leaks when Traefik recreates middleware
|
// to prevent duplication and goroutine leaks when Traefik recreates middleware
|
||||||
type ResourceManager struct {
|
type ResourceManager struct {
|
||||||
// HTTP clients shared across instances
|
|
||||||
httpClients map[string]*http.Client
|
|
||||||
clientsMu sync.RWMutex
|
|
||||||
|
|
||||||
// Caches shared across instances
|
|
||||||
caches map[string]interface{}
|
|
||||||
cachesMu sync.RWMutex
|
|
||||||
|
|
||||||
// Background tasks registry
|
|
||||||
tasks map[string]*BackgroundTask
|
|
||||||
tasksMu sync.RWMutex
|
|
||||||
|
|
||||||
// Goroutine pools for controlled concurrency
|
|
||||||
pools map[string]*GoroutinePool
|
|
||||||
poolsMu sync.RWMutex
|
|
||||||
|
|
||||||
// Reference counting for cleanup
|
|
||||||
references map[string]*int32
|
references map[string]*int32
|
||||||
referencesMu sync.RWMutex
|
caches map[string]interface{}
|
||||||
|
httpClients map[string]*http.Client
|
||||||
// Logger
|
tasks map[string]*BackgroundTask
|
||||||
logger *Logger
|
|
||||||
|
|
||||||
// Shutdown coordination
|
|
||||||
shutdownOnce sync.Once
|
|
||||||
shutdownChan chan struct{}
|
shutdownChan chan struct{}
|
||||||
|
pools map[string]*GoroutinePool
|
||||||
|
logger *Logger
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
cachesMu sync.RWMutex
|
||||||
|
referencesMu sync.RWMutex
|
||||||
|
poolsMu sync.RWMutex
|
||||||
|
tasksMu sync.RWMutex
|
||||||
|
clientsMu sync.RWMutex
|
||||||
|
shutdownOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetResourceManager returns the global singleton ResourceManager instance
|
// GetResourceManager returns the global singleton ResourceManager instance
|
||||||
@@ -338,17 +325,15 @@ func (rm *ResourceManager) Shutdown(ctx context.Context) error {
|
|||||||
|
|
||||||
// GoroutinePool provides a pool of workers for controlled concurrency
|
// GoroutinePool provides a pool of workers for controlled concurrency
|
||||||
type GoroutinePool struct {
|
type GoroutinePool struct {
|
||||||
maxWorkers int
|
|
||||||
taskQueue chan func()
|
taskQueue chan func()
|
||||||
workerWG sync.WaitGroup
|
|
||||||
shutdownOnce sync.Once
|
|
||||||
shutdownChan chan struct{}
|
shutdownChan chan struct{}
|
||||||
logger *Logger
|
logger *Logger
|
||||||
started int32
|
|
||||||
|
|
||||||
// Condition variable for efficient Wait() without busy-polling
|
|
||||||
taskCond *sync.Cond
|
taskCond *sync.Cond
|
||||||
pendingTasks int64 // atomic counter for pending tasks
|
workerWG sync.WaitGroup
|
||||||
|
maxWorkers int
|
||||||
|
pendingTasks int64
|
||||||
|
shutdownOnce sync.Once
|
||||||
|
started int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGoroutinePool creates a new goroutine pool with the specified max workers
|
// NewGoroutinePool creates a new goroutine pool with the specified max workers
|
||||||
@@ -517,10 +502,10 @@ func (p *GoroutinePool) Shutdown(ctx context.Context) error {
|
|||||||
// GenericCache provides a simple cache implementation for testing
|
// GenericCache provides a simple cache implementation for testing
|
||||||
type GenericCache struct {
|
type GenericCache struct {
|
||||||
data map[string]interface{}
|
data map[string]interface{}
|
||||||
mu sync.RWMutex
|
|
||||||
ttl time.Duration
|
|
||||||
logger *Logger
|
logger *Logger
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
|
ttl time.Duration
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGenericCache creates a new generic cache
|
// NewGenericCache creates a new generic cache
|
||||||
|
|||||||
+13
-20
@@ -9,26 +9,19 @@ import (
|
|||||||
|
|
||||||
// TestConfig manages test execution configuration and performance settings
|
// TestConfig manages test execution configuration and performance settings
|
||||||
type TestConfig struct {
|
type TestConfig struct {
|
||||||
// Test execution modes
|
MemoryThreshold float64
|
||||||
ExtendedTests bool // Run extended/stress tests
|
MaxConcurrency int
|
||||||
LongTests bool // Run long-running performance tests
|
MaxIterations int
|
||||||
QuickMode bool // Quick smoke tests only
|
DefaultTimeout time.Duration
|
||||||
|
GoroutineGrowth int
|
||||||
// Performance settings
|
CacheSize int
|
||||||
MaxConcurrency int // Maximum concurrent operations
|
CleanupInterval time.Duration
|
||||||
MaxIterations int // Maximum test iterations
|
LongTests bool
|
||||||
DefaultTimeout time.Duration // Default test timeout
|
QuickMode bool
|
||||||
MemoryThreshold float64 // Memory growth threshold in MB
|
ExtendedTests bool
|
||||||
GoroutineGrowth int // Acceptable goroutine growth
|
MemoryStressTest bool
|
||||||
|
ConcurrencyTest bool
|
||||||
// Cache settings for tests
|
LeakDetection bool
|
||||||
CacheSize int // Default cache size for tests
|
|
||||||
CleanupInterval time.Duration // Cleanup interval for tests
|
|
||||||
|
|
||||||
// Environment-specific overrides
|
|
||||||
MemoryStressTest bool // Enable memory stress tests
|
|
||||||
ConcurrencyTest bool // Enable high concurrency tests
|
|
||||||
LeakDetection bool // Enable memory leak detection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestConfig creates a test configuration based on flags and environment
|
// NewTestConfig creates a test configuration based on flags and environment
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ type TestFramework struct {
|
|||||||
server *httptest.Server
|
server *httptest.Server
|
||||||
oidc *TraefikOidc
|
oidc *TraefikOidc
|
||||||
config *Config
|
config *Config
|
||||||
cleanup []func()
|
|
||||||
mocks *TestMocks
|
mocks *TestMocks
|
||||||
fixtures *TestFixtures
|
fixtures *TestFixtures
|
||||||
privateKey *rsa.PrivateKey
|
privateKey *rsa.PrivateKey
|
||||||
publicKey *rsa.PublicKey
|
publicKey *rsa.PublicKey
|
||||||
|
cleanup []func()
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,12 +457,12 @@ func GetTestFramework() *TestFramework {
|
|||||||
|
|
||||||
// TestScenario represents a test scenario
|
// TestScenario represents a test scenario
|
||||||
type TestScenario struct {
|
type TestScenario struct {
|
||||||
Name string
|
|
||||||
Setup func(*TestFramework)
|
Setup func(*TestFramework)
|
||||||
Request func(*TestFramework) *http.Request
|
Request func(*TestFramework) *http.Request
|
||||||
ExpectedStatus int
|
|
||||||
ExpectedBody string
|
|
||||||
Validate func(*TestFramework, *httptest.ResponseRecorder)
|
Validate func(*TestFramework, *httptest.ResponseRecorder)
|
||||||
|
Name string
|
||||||
|
ExpectedBody string
|
||||||
|
ExpectedStatus int
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunScenarios executes a set of test scenarios
|
// RunScenarios executes a set of test scenarios
|
||||||
|
|||||||
+15
-15
@@ -17,10 +17,10 @@ import (
|
|||||||
|
|
||||||
// GlobalTestCleanup tracks and cleans up test resources
|
// GlobalTestCleanup tracks and cleans up test resources
|
||||||
type GlobalTestCleanup struct {
|
type GlobalTestCleanup struct {
|
||||||
mu sync.Mutex
|
|
||||||
servers []*httptest.Server
|
servers []*httptest.Server
|
||||||
tasks []*BackgroundTask
|
tasks []*BackgroundTask
|
||||||
caches []interface{ Close() }
|
caches []interface{ Close() }
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var globalCleanup = &GlobalTestCleanup{}
|
var globalCleanup = &GlobalTestCleanup{}
|
||||||
@@ -187,13 +187,13 @@ func GetTestDuration(normal time.Duration) time.Duration {
|
|||||||
|
|
||||||
// UnifiedMockSession provides a comprehensive mock for the Session interface
|
// UnifiedMockSession provides a comprehensive mock for the Session interface
|
||||||
type UnifiedMockSession struct {
|
type UnifiedMockSession struct {
|
||||||
mu sync.RWMutex
|
|
||||||
data map[string]interface{}
|
data map[string]interface{}
|
||||||
callCounts map[string]int64
|
callCounts map[string]int64
|
||||||
errors map[string]error
|
errors map[string]error
|
||||||
delays map[string]time.Duration
|
delays map[string]time.Duration
|
||||||
destroyed bool
|
|
||||||
destroyCount int64
|
destroyCount int64
|
||||||
|
mu sync.RWMutex
|
||||||
|
destroyed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUnifiedMockSession creates a new mock session with default behavior
|
// NewUnifiedMockSession creates a new mock session with default behavior
|
||||||
@@ -326,13 +326,13 @@ func (m *UnifiedMockSession) GetDestroyCount() int64 {
|
|||||||
|
|
||||||
// UnifiedMockTokenVerifier provides a comprehensive mock for token verification
|
// UnifiedMockTokenVerifier provides a comprehensive mock for token verification
|
||||||
type UnifiedMockTokenVerifier struct {
|
type UnifiedMockTokenVerifier struct {
|
||||||
mu sync.RWMutex
|
|
||||||
validTokens map[string]bool
|
validTokens map[string]bool
|
||||||
tokenMetadata map[string]map[string]interface{}
|
tokenMetadata map[string]map[string]interface{}
|
||||||
callCounts map[string]int64
|
callCounts map[string]int64
|
||||||
errors map[string]error
|
errors map[string]error
|
||||||
delays map[string]time.Duration
|
delays map[string]time.Duration
|
||||||
verificationFunc func(string) error
|
verificationFunc func(string) error
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUnifiedMockTokenVerifier creates a new mock token verifier
|
// NewUnifiedMockTokenVerifier creates a new mock token verifier
|
||||||
@@ -414,19 +414,19 @@ func (m *UnifiedMockTokenVerifier) VerifyToken(token string) error {
|
|||||||
|
|
||||||
// UnifiedMockTokenCache provides a comprehensive mock for token caching
|
// UnifiedMockTokenCache provides a comprehensive mock for token caching
|
||||||
type UnifiedMockTokenCache struct {
|
type UnifiedMockTokenCache struct {
|
||||||
mu sync.RWMutex
|
|
||||||
cache map[string]TestCacheEntry
|
cache map[string]TestCacheEntry
|
||||||
callCounts map[string]int64
|
callCounts map[string]int64
|
||||||
errors map[string]error
|
errors map[string]error
|
||||||
delays map[string]time.Duration
|
delays map[string]time.Duration
|
||||||
hitRate float64
|
hitRate float64
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCacheEntry represents a cached token entry for testing
|
// TestCacheEntry represents a cached token entry for testing
|
||||||
type TestCacheEntry struct {
|
type TestCacheEntry struct {
|
||||||
Token string
|
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUnifiedMockTokenCache creates a new mock token cache
|
// NewUnifiedMockTokenCache creates a new mock token cache
|
||||||
@@ -539,39 +539,39 @@ func (m *UnifiedMockTokenCache) Clear() {
|
|||||||
|
|
||||||
// TableTestCase represents a standardized test case structure
|
// TableTestCase represents a standardized test case structure
|
||||||
type TableTestCase struct {
|
type TableTestCase struct {
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
Input interface{}
|
Input interface{}
|
||||||
Expected interface{}
|
Expected interface{}
|
||||||
ExpectedError error
|
ExpectedError error
|
||||||
Setup func(*testing.T) error
|
Setup func(*testing.T) error
|
||||||
Teardown func(*testing.T) error
|
Teardown func(*testing.T) error
|
||||||
Timeout time.Duration
|
Name string
|
||||||
|
Description string
|
||||||
SkipReason string
|
SkipReason string
|
||||||
Tags []string
|
Tags []string
|
||||||
|
Timeout time.Duration
|
||||||
Parallel bool
|
Parallel bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryLeakTestCase represents a test case specifically for memory leak detection
|
// MemoryLeakTestCase represents a test case specifically for memory leak detection
|
||||||
type MemoryLeakTestCase struct {
|
type MemoryLeakTestCase struct {
|
||||||
|
Operation func() error
|
||||||
|
Setup func() error
|
||||||
|
Teardown func() error
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Operation func() error
|
|
||||||
Iterations int
|
Iterations int
|
||||||
MaxGoroutineGrowth int
|
MaxGoroutineGrowth int
|
||||||
MaxMemoryGrowthMB float64
|
MaxMemoryGrowthMB float64
|
||||||
Setup func() error
|
|
||||||
Teardown func() error
|
|
||||||
GCBetweenRuns bool
|
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
GCBetweenRuns bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSuiteRunner provides utilities for running table-driven tests
|
// TestSuiteRunner provides utilities for running table-driven tests
|
||||||
type TestSuiteRunner struct {
|
type TestSuiteRunner struct {
|
||||||
parallelTests bool
|
|
||||||
timeout time.Duration
|
|
||||||
beforeEach func(*testing.T)
|
beforeEach func(*testing.T)
|
||||||
afterEach func(*testing.T)
|
afterEach func(*testing.T)
|
||||||
|
timeout time.Duration
|
||||||
|
parallelTests bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestSuiteRunner creates a new test suite runner
|
// NewTestSuiteRunner creates a new test suite runner
|
||||||
|
|||||||
+1
-1
@@ -20,9 +20,9 @@ func generateRandomString(length int) string {
|
|||||||
// Test createCaseInsensitiveStringMap function
|
// Test createCaseInsensitiveStringMap function
|
||||||
func TestCreateCaseInsensitiveStringMap(t *testing.T) {
|
func TestCreateCaseInsensitiveStringMap(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
expected map[string]struct{}
|
||||||
name string
|
name string
|
||||||
items []string
|
items []string
|
||||||
expected map[string]struct{}
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Mixed case items",
|
name: "Mixed case items",
|
||||||
|
|||||||
+12
-12
@@ -16,18 +16,18 @@ import (
|
|||||||
// IntrospectionResponse represents the response from an OAuth 2.0 token introspection endpoint.
|
// IntrospectionResponse represents the response from an OAuth 2.0 token introspection endpoint.
|
||||||
// Per RFC 7662, this contains information about the token's validity and properties.
|
// Per RFC 7662, this contains information about the token's validity and properties.
|
||||||
type IntrospectionResponse struct {
|
type IntrospectionResponse struct {
|
||||||
Active bool `json:"active"` // REQUIRED - whether the token is currently active
|
Scope string `json:"scope,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"` // Space-separated list of scopes
|
ClientID string `json:"client_id,omitempty"`
|
||||||
ClientID string `json:"client_id,omitempty"` // Client identifier for the token
|
Username string `json:"username,omitempty"`
|
||||||
Username string `json:"username,omitempty"` // Human-readable identifier for the resource owner
|
TokenType string `json:"token_type,omitempty"`
|
||||||
TokenType string `json:"token_type,omitempty"` // Type of token (e.g., "Bearer")
|
Sub string `json:"sub,omitempty"`
|
||||||
Exp int64 `json:"exp,omitempty"` // Expiration time (seconds since epoch)
|
Aud string `json:"aud,omitempty"`
|
||||||
Iat int64 `json:"iat,omitempty"` // Issued at time (seconds since epoch)
|
Iss string `json:"iss,omitempty"`
|
||||||
Nbf int64 `json:"nbf,omitempty"` // Not before time (seconds since epoch)
|
Jti string `json:"jti,omitempty"`
|
||||||
Sub string `json:"sub,omitempty"` // Subject of the token
|
Exp int64 `json:"exp,omitempty"`
|
||||||
Aud string `json:"aud,omitempty"` // Intended audience
|
Iat int64 `json:"iat,omitempty"`
|
||||||
Iss string `json:"iss,omitempty"` // Issuer
|
Nbf int64 `json:"nbf,omitempty"`
|
||||||
Jti string `json:"jti,omitempty"` // JWT ID
|
Active bool `json:"active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// introspectToken performs OAuth 2.0 Token Introspection (RFC 7662) for an opaque token.
|
// introspectToken performs OAuth 2.0 Token Introspection (RFC 7662) for an opaque token.
|
||||||
|
|||||||
+10
-26
@@ -8,37 +8,21 @@ import (
|
|||||||
|
|
||||||
// TokenResilienceConfig centralizes resilience configuration for token operations
|
// TokenResilienceConfig centralizes resilience configuration for token operations
|
||||||
type TokenResilienceConfig struct {
|
type TokenResilienceConfig struct {
|
||||||
// Circuit breaker configuration for token operations
|
MetadataCacheConfig MetadataCacheResilienceConfig
|
||||||
CircuitBreakerEnabled bool
|
RetryConfig RetryConfig
|
||||||
CircuitBreakerConfig CircuitBreakerConfig
|
CircuitBreakerConfig CircuitBreakerConfig
|
||||||
|
CircuitBreakerEnabled bool
|
||||||
// Retry configuration for token operations
|
RetryEnabled bool
|
||||||
RetryEnabled bool
|
|
||||||
RetryConfig RetryConfig
|
|
||||||
|
|
||||||
// Metadata cache progressive grace period configuration
|
|
||||||
MetadataCacheConfig MetadataCacheResilienceConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetadataCacheResilienceConfig defines resilience settings for metadata cache
|
// MetadataCacheResilienceConfig defines resilience settings for metadata cache
|
||||||
type MetadataCacheResilienceConfig struct {
|
type MetadataCacheResilienceConfig struct {
|
||||||
// EnableProgressiveGracePeriod allows extending cache TTL on failures
|
SecurityCriticalFields []string
|
||||||
EnableProgressiveGracePeriod bool
|
InitialGracePeriod time.Duration
|
||||||
|
ExtendedGracePeriod time.Duration
|
||||||
// InitialGracePeriod is the first extension when service is unavailable (5 minutes)
|
MaxGracePeriod time.Duration
|
||||||
InitialGracePeriod time.Duration
|
|
||||||
|
|
||||||
// ExtendedGracePeriod is the second extension for continued failures (15 minutes)
|
|
||||||
ExtendedGracePeriod time.Duration
|
|
||||||
|
|
||||||
// MaxGracePeriod is the maximum extension allowed (30 minutes for normal, 15 for security-critical)
|
|
||||||
MaxGracePeriod time.Duration
|
|
||||||
|
|
||||||
// SecurityCriticalMaxGracePeriod enforces Allan's security limit for critical metadata
|
|
||||||
SecurityCriticalMaxGracePeriod time.Duration
|
SecurityCriticalMaxGracePeriod time.Duration
|
||||||
|
EnableProgressiveGracePeriod bool
|
||||||
// SecurityCriticalFields defines which metadata fields are security-critical
|
|
||||||
SecurityCriticalFields []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultTokenResilienceConfig returns the default resilience configuration for token operations
|
// DefaultTokenResilienceConfig returns the default resilience configuration for token operations
|
||||||
@@ -90,11 +74,11 @@ func DefaultMetadataCacheResilienceConfig() MetadataCacheResilienceConfig {
|
|||||||
|
|
||||||
// TokenResilienceManager coordinates resilience mechanisms for token operations
|
// TokenResilienceManager coordinates resilience mechanisms for token operations
|
||||||
type TokenResilienceManager struct {
|
type TokenResilienceManager struct {
|
||||||
config TokenResilienceConfig
|
|
||||||
errorRecoveryManager *ErrorRecoveryManager
|
errorRecoveryManager *ErrorRecoveryManager
|
||||||
circuitBreaker *CircuitBreaker
|
circuitBreaker *CircuitBreaker
|
||||||
retryExecutor *RetryExecutor
|
retryExecutor *RetryExecutor
|
||||||
logger *Logger
|
logger *Logger
|
||||||
|
config TokenResilienceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTokenResilienceManager creates a new token resilience manager
|
// NewTokenResilienceManager creates a new token resilience manager
|
||||||
|
|||||||
+25
-25
@@ -26,10 +26,10 @@ import (
|
|||||||
|
|
||||||
// Test tokens used across multiple test files
|
// Test tokens used across multiple test files
|
||||||
var (
|
var (
|
||||||
ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU"
|
ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" // trufflehog:ignore
|
||||||
ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU"
|
ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU" // trufflehog:ignore
|
||||||
ValidRefreshToken = "refresh_token_abc123"
|
ValidRefreshToken = "refresh_token_abc123"
|
||||||
MinimalValidJWT = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0."
|
MinimalValidJWT = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0." // trufflehog:ignore
|
||||||
InvalidTokenOneDot = "invalid.token"
|
InvalidTokenOneDot = "invalid.token"
|
||||||
InvalidTokenNoDots = "invalidtoken"
|
InvalidTokenNoDots = "invalidtoken"
|
||||||
InvalidTokenThreeDots = "invalid..token"
|
InvalidTokenThreeDots = "invalid..token"
|
||||||
@@ -43,8 +43,8 @@ type TestTokens struct {
|
|||||||
|
|
||||||
func NewTestTokens() *TestTokens {
|
func NewTestTokens() *TestTokens {
|
||||||
return &TestTokens{
|
return &TestTokens{
|
||||||
validJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU",
|
validJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjozMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", // trufflehog:ignore
|
||||||
expiredJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjoxMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU",
|
expiredJWT: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjoxMDAwMDAwMDAwLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.dGVzdC1zaWduYXR1cmU", // trufflehog:ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,12 +137,12 @@ func TestOpaqueTokenDetection(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
token string
|
token string
|
||||||
isOpaque bool
|
|
||||||
description string
|
description string
|
||||||
|
isOpaque bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "JWT token with 3 parts",
|
name: "JWT token with 3 parts",
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
|
||||||
isOpaque: false,
|
isOpaque: false,
|
||||||
description: "Standard JWT with header.payload.signature",
|
description: "Standard JWT with header.payload.signature",
|
||||||
},
|
},
|
||||||
@@ -218,7 +218,7 @@ func TestOpaqueTokenValidation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid JWT token",
|
name: "Valid JWT token",
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
|
||||||
wantError: false,
|
wantError: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -253,8 +253,8 @@ func TestOpaqueTokenStorage(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
token string
|
token string
|
||||||
shouldStore bool
|
|
||||||
description string
|
description string
|
||||||
|
shouldStore bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid opaque token",
|
name: "Valid opaque token",
|
||||||
@@ -264,7 +264,7 @@ func TestOpaqueTokenStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid JWT token",
|
name: "Valid JWT token",
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", // trufflehog:ignore
|
||||||
shouldStore: true,
|
shouldStore: true,
|
||||||
description: "Standard JWT with three parts",
|
description: "Standard JWT with three parts",
|
||||||
},
|
},
|
||||||
@@ -754,11 +754,11 @@ func TestDetectTokenType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
|
||||||
jwt *JWT
|
jwt *JWT
|
||||||
|
name string
|
||||||
token string
|
token string
|
||||||
expectedID bool
|
|
||||||
description string
|
description string
|
||||||
|
expectedID bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "ID token with nonce",
|
name: "ID token with nonce",
|
||||||
@@ -1233,16 +1233,16 @@ func TestExtractTime(t *testing.T) {
|
|||||||
validator := NewTokenValidator(nil)
|
validator := NewTokenValidator(nil)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
claim interface{}
|
claim interface{}
|
||||||
|
name string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{"float64", float64(1609459200), true},
|
{name: "float64", claim: float64(1609459200), expected: true},
|
||||||
{"int64", int64(1609459200), true},
|
{name: "int64", claim: int64(1609459200), expected: true},
|
||||||
{"int", int(1609459200), true},
|
{name: "int", claim: int(1609459200), expected: true},
|
||||||
{"string", "not a timestamp", false},
|
{name: "string", claim: "not a timestamp", expected: false},
|
||||||
{"nil", nil, false},
|
{name: "nil", claim: nil, expected: false},
|
||||||
{"map", map[string]interface{}{}, false},
|
{name: "map", claim: map[string]interface{}{}, expected: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -1556,11 +1556,11 @@ func TestTokenCorruption(t *testing.T) {
|
|||||||
validJWT := testTokens.CreateLargeValidJWT(100)
|
validJWT := testTokens.CreateLargeValidJWT(100)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
corruptionScenario func(*SessionData)
|
||||||
name string
|
name string
|
||||||
tokenSize int
|
tokenSize int
|
||||||
iterations int
|
iterations int
|
||||||
expectConsistent bool
|
expectConsistent bool
|
||||||
corruptionScenario func(*SessionData)
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Small token - multiple retrievals",
|
name: "Small token - multiple retrievals",
|
||||||
@@ -1803,14 +1803,14 @@ func TestTokenValidation(t *testing.T) {
|
|||||||
t.Run("TokenExpiryValidation", func(t *testing.T) {
|
t.Run("TokenExpiryValidation", func(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
exp time.Time
|
exp time.Time
|
||||||
|
name string
|
||||||
expectValid bool
|
expectValid bool
|
||||||
}{
|
}{
|
||||||
{"Future expiry", now.Add(time.Hour), true},
|
{name: "Future expiry", exp: now.Add(time.Hour), expectValid: true},
|
||||||
{"Just expired", now.Add(-time.Second), false},
|
{name: "Just expired", exp: now.Add(-time.Second), expectValid: false},
|
||||||
{"Long expired", now.Add(-24 * time.Hour), false},
|
{name: "Long expired", exp: now.Add(-24 * time.Hour), expectValid: false},
|
||||||
{"Far future", now.Add(365 * 24 * time.Hour), true},
|
{name: "Far future", exp: now.Add(365 * 24 * time.Hour), expectValid: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
+3
-3
@@ -27,12 +27,12 @@ func NewTokenValidator(logger *Logger) *TokenValidator {
|
|||||||
|
|
||||||
// TokenValidationResult contains the result of token validation
|
// TokenValidationResult contains the result of token validation
|
||||||
type TokenValidationResult struct {
|
type TokenValidationResult struct {
|
||||||
Valid bool
|
Error error
|
||||||
TokenType string
|
|
||||||
Claims map[string]interface{}
|
Claims map[string]interface{}
|
||||||
Expiry *time.Time
|
Expiry *time.Time
|
||||||
IssuedAt *time.Time
|
IssuedAt *time.Time
|
||||||
Error error
|
TokenType string
|
||||||
|
Valid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateToken performs comprehensive token validation
|
// ValidateToken performs comprehensive token validation
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ type ProviderMetadata struct {
|
|||||||
JWKSURL string `json:"jwks_uri"`
|
JWKSURL string `json:"jwks_uri"`
|
||||||
RevokeURL string `json:"revocation_endpoint"`
|
RevokeURL string `json:"revocation_endpoint"`
|
||||||
EndSessionURL string `json:"end_session_endpoint"`
|
EndSessionURL string `json:"end_session_endpoint"`
|
||||||
IntrospectionURL string `json:"introspection_endpoint,omitempty"` // OAuth 2.0 Token Introspection (RFC 7662)
|
IntrospectionURL string `json:"introspection_endpoint,omitempty"`
|
||||||
ScopesSupported []string `json:"scopes_supported,omitempty"` // Supported scopes from discovery
|
RegistrationURL string `json:"registration_endpoint,omitempty"`
|
||||||
RegistrationURL string `json:"registration_endpoint,omitempty"` // OIDC Dynamic Client Registration (RFC 7591)
|
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TraefikOidc is the main middleware struct that implements OIDC authentication for Traefik.
|
// TraefikOidc is the main middleware struct that implements OIDC authentication for Traefik.
|
||||||
@@ -65,16 +65,18 @@ type ProviderMetadata struct {
|
|||||||
// the complete authentication flow. It's designed to work seamlessly with Traefik's
|
// the complete authentication flow. It's designed to work seamlessly with Traefik's
|
||||||
// plugin system and provides flexible configuration options.
|
// plugin system and provides flexible configuration options.
|
||||||
type TraefikOidc struct {
|
type TraefikOidc struct {
|
||||||
|
lastMetadataRetryTime time.Time
|
||||||
jwkCache JWKCacheInterface
|
jwkCache JWKCacheInterface
|
||||||
jwtVerifier JWTVerifier
|
jwtVerifier JWTVerifier
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
tokenVerifier TokenVerifier
|
tokenVerifier TokenVerifier
|
||||||
next http.Handler
|
next http.Handler
|
||||||
tokenExchanger TokenExchanger
|
tokenExchanger TokenExchanger
|
||||||
|
tokenBlacklist CacheInterface
|
||||||
|
tokenTypeCache CacheInterface
|
||||||
|
introspectionCache CacheInterface
|
||||||
initComplete chan struct{}
|
initComplete chan struct{}
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
tokenBlacklist CacheInterface
|
|
||||||
tokenTypeCache CacheInterface // Cache for token type detection results
|
|
||||||
headerTemplates map[string]*template.Template
|
headerTemplates map[string]*template.Template
|
||||||
sessionManager *SessionManager
|
sessionManager *SessionManager
|
||||||
tokenCleanupStopChan chan struct{}
|
tokenCleanupStopChan chan struct{}
|
||||||
@@ -94,50 +96,46 @@ type TraefikOidc struct {
|
|||||||
errorRecoveryManager *ErrorRecoveryManager
|
errorRecoveryManager *ErrorRecoveryManager
|
||||||
tokenResilienceManager *TokenResilienceManager
|
tokenResilienceManager *TokenResilienceManager
|
||||||
goroutineWG *sync.WaitGroup
|
goroutineWG *sync.WaitGroup
|
||||||
clientSecret string
|
dcrConfig *DynamicClientRegistrationConfig
|
||||||
clientID string
|
dynamicClientRegistrar *DynamicClientRegistrar
|
||||||
audience string // Expected JWT audience, defaults to clientID
|
scopeFilter *ScopeFilter
|
||||||
roleClaimName string // JWT claim name for extracting roles, defaults to "roles"
|
securityHeadersApplier func(http.ResponseWriter, *http.Request)
|
||||||
groupClaimName string // JWT claim name for extracting groups, defaults to "groups"
|
userIdentifierClaim string
|
||||||
userIdentifierClaim string // JWT claim for user identification, defaults to "email"
|
revocationURL string
|
||||||
name string
|
name string
|
||||||
redirURLPath string
|
redirURLPath string
|
||||||
logoutURLPath string
|
logoutURLPath string
|
||||||
metadataMu sync.RWMutex // Protects metadata endpoint fields
|
|
||||||
tokenURL string
|
tokenURL string
|
||||||
authURL string
|
authURL string
|
||||||
endSessionURL string
|
endSessionURL string
|
||||||
postLogoutRedirectURI string
|
postLogoutRedirectURI string
|
||||||
jwksURL string
|
jwksURL string
|
||||||
issuerURL string
|
issuerURL string
|
||||||
revocationURL string
|
groupClaimName string
|
||||||
introspectionURL string // OAuth 2.0 Token Introspection endpoint (RFC 7662)
|
introspectionURL string
|
||||||
providerURL string
|
providerURL string
|
||||||
|
roleClaimName string
|
||||||
|
audience string
|
||||||
|
clientID string
|
||||||
|
clientSecret string
|
||||||
|
registrationURL string
|
||||||
|
scopesSupported []string
|
||||||
scopes []string
|
scopes []string
|
||||||
refreshGracePeriod time.Duration
|
refreshGracePeriod time.Duration
|
||||||
introspectionCache CacheInterface // Cache for token introspection results
|
metadataMu sync.RWMutex
|
||||||
shutdownOnce sync.Once
|
shutdownOnce sync.Once
|
||||||
|
metadataRetryMutex sync.Mutex
|
||||||
firstRequestMutex sync.Mutex
|
firstRequestMutex sync.Mutex
|
||||||
forceHTTPS bool
|
minimalHeaders bool
|
||||||
enablePKCE bool
|
|
||||||
overrideScopes bool
|
|
||||||
strictAudienceValidation bool // Prevents Scenario 2 fallback to ID token
|
|
||||||
allowOpaqueTokens bool // Enables opaque token support via introspection
|
|
||||||
requireTokenIntrospection bool // Forces introspection for opaque tokens
|
|
||||||
disableReplayDetection bool // Disables JTI-based replay detection for multi-replica deployments
|
|
||||||
suppressDiagnosticLogs bool
|
|
||||||
firstRequestReceived bool
|
firstRequestReceived bool
|
||||||
|
requireTokenIntrospection bool
|
||||||
metadataRefreshStarted bool
|
metadataRefreshStarted bool
|
||||||
lastMetadataRetryTime time.Time // Track last metadata retry for failed state recovery
|
allowPrivateIPAddresses bool
|
||||||
metadataRetryMutex sync.Mutex // Protects lastMetadataRetryTime
|
disableReplayDetection bool
|
||||||
allowPrivateIPAddresses bool // Allow private IP addresses in URLs (for internal networks)
|
allowOpaqueTokens bool
|
||||||
minimalHeaders bool // Reduce headers to prevent 431 errors
|
strictAudienceValidation bool
|
||||||
securityHeadersApplier func(http.ResponseWriter, *http.Request)
|
overrideScopes bool
|
||||||
scopeFilter *ScopeFilter // NEW - for discovery-based scope filtering
|
enablePKCE bool
|
||||||
scopesSupported []string // NEW - from provider metadata
|
forceHTTPS bool
|
||||||
|
suppressDiagnosticLogs bool
|
||||||
// Dynamic Client Registration (RFC 7591)
|
|
||||||
dynamicClientRegistrar *DynamicClientRegistrar
|
|
||||||
dcrConfig *DynamicClientRegistrationConfig
|
|
||||||
registrationURL string // OIDC Dynamic Client Registration endpoint
|
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-48
@@ -25,27 +25,21 @@ const (
|
|||||||
|
|
||||||
// UniversalCacheConfig provides configuration for the universal cache
|
// UniversalCacheConfig provides configuration for the universal cache
|
||||||
type UniversalCacheConfig struct {
|
type UniversalCacheConfig struct {
|
||||||
|
Strategy CacheStrategy
|
||||||
|
Logger *Logger
|
||||||
|
JWKConfig *JWKCacheConfig
|
||||||
|
MetadataConfig *MetadataCacheConfig
|
||||||
|
TokenConfig *TokenCacheConfig
|
||||||
Type CacheType
|
Type CacheType
|
||||||
MaxSize int
|
|
||||||
MaxMemoryBytes int64
|
|
||||||
DefaultTTL time.Duration
|
DefaultTTL time.Duration
|
||||||
CleanupInterval time.Duration
|
CleanupInterval time.Duration
|
||||||
EnableCompression bool
|
MaxMemoryBytes int64
|
||||||
|
MaxSize int
|
||||||
|
EnableAutoCleanup bool
|
||||||
|
EnableMemoryLimit bool
|
||||||
EnableMetrics bool
|
EnableMetrics bool
|
||||||
EnableAutoCleanup bool // For backward compatibility
|
EnableCompression bool
|
||||||
EnableMemoryLimit bool // For backward compatibility
|
SkipAutoCleanup bool
|
||||||
Logger *Logger
|
|
||||||
Strategy CacheStrategy // For backward compatibility
|
|
||||||
|
|
||||||
// SkipAutoCleanup skips starting the per-cache cleanup goroutine.
|
|
||||||
// Use this when cleanup is managed externally (e.g., by UniversalCacheManager)
|
|
||||||
// to reduce goroutine count and consolidate cleanup operations.
|
|
||||||
SkipAutoCleanup bool
|
|
||||||
|
|
||||||
// Type-specific configurations
|
|
||||||
TokenConfig *TokenCacheConfig
|
|
||||||
MetadataConfig *MetadataCacheConfig
|
|
||||||
JWKConfig *JWKCacheConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenCacheConfig provides token-specific cache configuration
|
// TokenCacheConfig provides token-specific cache configuration
|
||||||
@@ -57,11 +51,11 @@ type TokenCacheConfig struct {
|
|||||||
|
|
||||||
// MetadataCacheConfig provides metadata-specific cache configuration
|
// MetadataCacheConfig provides metadata-specific cache configuration
|
||||||
type MetadataCacheConfig struct {
|
type MetadataCacheConfig struct {
|
||||||
|
SecurityCriticalFields []string
|
||||||
GracePeriod time.Duration
|
GracePeriod time.Duration
|
||||||
ExtendedGracePeriod time.Duration
|
ExtendedGracePeriod time.Duration
|
||||||
MaxGracePeriod time.Duration
|
MaxGracePeriod time.Duration
|
||||||
SecurityCriticalMaxGracePeriod time.Duration
|
SecurityCriticalMaxGracePeriod time.Duration
|
||||||
SecurityCriticalFields []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWKCacheConfig provides JWK-specific cache configuration
|
// JWKCacheConfig provides JWK-specific cache configuration
|
||||||
@@ -73,48 +67,36 @@ type JWKCacheConfig struct {
|
|||||||
|
|
||||||
// CacheItem represents a single cache entry
|
// CacheItem represents a single cache entry
|
||||||
type CacheItem struct {
|
type CacheItem struct {
|
||||||
Key string
|
|
||||||
Value interface{}
|
|
||||||
Size int64
|
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
LastAccessed time.Time
|
LastAccessed time.Time
|
||||||
AccessCount int64
|
Value interface{}
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
element *list.Element
|
||||||
|
Key string
|
||||||
CacheType CacheType
|
CacheType CacheType
|
||||||
|
Size int64
|
||||||
// Type-specific metadata
|
AccessCount int64
|
||||||
Metadata map[string]interface{}
|
|
||||||
|
|
||||||
// LRU list element reference
|
|
||||||
element *list.Element
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UniversalCache provides a single, unified cache implementation
|
// UniversalCache provides a single, unified cache implementation
|
||||||
// that replaces all other cache types
|
// that replaces all other cache types
|
||||||
type UniversalCache struct {
|
type UniversalCache struct {
|
||||||
mu sync.RWMutex
|
config UniversalCacheConfig
|
||||||
items map[string]*CacheItem
|
|
||||||
lruList *list.List
|
|
||||||
config UniversalCacheConfig
|
|
||||||
logger *Logger
|
|
||||||
|
|
||||||
// Backend for distributed caching (NEW)
|
|
||||||
backend backends.CacheBackend
|
|
||||||
ownsBackend bool // If true, cache should close backend on Close(); if false, backend is shared
|
|
||||||
|
|
||||||
// Memory management
|
|
||||||
currentSize int64
|
|
||||||
currentMemory int64
|
|
||||||
|
|
||||||
// Metrics
|
|
||||||
hits int64
|
|
||||||
misses int64
|
|
||||||
evictions int64
|
|
||||||
|
|
||||||
// Lifecycle management
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
backend backends.CacheBackend
|
||||||
|
logger *Logger
|
||||||
|
lruList *list.List
|
||||||
|
items map[string]*CacheItem
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
cleanupTicker *time.Ticker
|
cleanupTicker *time.Ticker
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
currentSize int64
|
||||||
|
currentMemory int64
|
||||||
|
hits int64
|
||||||
|
misses int64
|
||||||
|
evictions int64
|
||||||
|
mu sync.RWMutex
|
||||||
|
ownsBackend bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUniversalCache creates a new universal cache instance
|
// NewUniversalCache creates a new universal cache instance
|
||||||
|
|||||||
@@ -13,22 +13,20 @@ import (
|
|||||||
// It runs a single consolidated cleanup goroutine for all caches, reducing
|
// It runs a single consolidated cleanup goroutine for all caches, reducing
|
||||||
// goroutine count and CPU overhead compared to per-cache cleanup routines.
|
// goroutine count and CPU overhead compared to per-cache cleanup routines.
|
||||||
type UniversalCacheManager struct {
|
type UniversalCacheManager struct {
|
||||||
tokenCache *UniversalCache
|
sharedBackend backends.CacheBackend
|
||||||
blacklistCache *UniversalCache
|
ctx context.Context
|
||||||
metadataCache *UniversalCache
|
tokenTypeCache *UniversalCache
|
||||||
jwkCache *UniversalCache
|
jwkCache *UniversalCache
|
||||||
sessionCache *UniversalCache
|
sessionCache *UniversalCache
|
||||||
introspectionCache *UniversalCache // OAuth 2.0 Token Introspection cache (RFC 7662)
|
introspectionCache *UniversalCache
|
||||||
tokenTypeCache *UniversalCache // Cache for token type detection results
|
tokenCache *UniversalCache
|
||||||
sharedBackend backends.CacheBackend // Shared backend (Redis) that should be closed by manager, not individual caches
|
metadataCache *UniversalCache
|
||||||
mu sync.RWMutex
|
|
||||||
logger *Logger
|
logger *Logger
|
||||||
|
blacklistCache *UniversalCache
|
||||||
// Consolidated cleanup management
|
cancel context.CancelFunc
|
||||||
ctx context.Context
|
wg sync.WaitGroup
|
||||||
cancel context.CancelFunc
|
mu sync.RWMutex
|
||||||
wg sync.WaitGroup
|
cleanupStarted bool
|
||||||
cleanupStarted bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
Reference in New Issue
Block a user