mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-11-04 07:53:59 +00:00
Compare commits
2
Commits
v0.6.0
...
v0.6.2-beta5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa0cd708b | ||
|
|
99881f5837 |
+18
-13
@@ -123,19 +123,24 @@ func (t *TraefikOidc) exchangeTokens(ctx context.Context, grantType string, code
|
|||||||
data.Set("refresh_token", codeOrToken)
|
data.Set("refresh_token", codeOrToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a cookie jar for this request to handle redirects with cookies
|
// Use the reusable token HTTP client, fallback to creating one if not initialized
|
||||||
jar, _ := cookiejar.New(nil)
|
client := t.tokenHTTPClient
|
||||||
client := &http.Client{
|
if client == nil {
|
||||||
Transport: t.httpClient.Transport,
|
// Fallback for tests or incomplete initialization - create a temporary client
|
||||||
Timeout: t.httpClient.Timeout,
|
// with the same behavior as the original implementation
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
jar, _ := cookiejar.New(nil)
|
||||||
// Always follow redirects for OIDC endpoints
|
client = &http.Client{
|
||||||
if len(via) >= 50 {
|
Transport: t.httpClient.Transport,
|
||||||
return fmt.Errorf("stopped after 50 redirects")
|
Timeout: t.httpClient.Timeout,
|
||||||
}
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
return nil
|
// Always follow redirects for OIDC endpoints
|
||||||
},
|
if len(via) >= 50 {
|
||||||
Jar: jar,
|
return fmt.Errorf("stopped after 50 redirects")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Jar: jar,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", t.tokenURL, strings.NewReader(data.Encode()))
|
req, err := http.NewRequestWithContext(ctx, "POST", t.tokenURL, strings.NewReader(data.Encode()))
|
||||||
|
|||||||
@@ -17,26 +17,14 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
replayCacheMu sync.Mutex
|
replayCacheMu sync.Mutex
|
||||||
replayCache = make(map[string]time.Time)
|
replayCache *Cache // Replace unbounded map with bounded Cache
|
||||||
)
|
)
|
||||||
|
|
||||||
// cleanupReplayCache iterates through the replay cache and removes entries
|
// initReplayCache initializes the global replay cache with size limit
|
||||||
// whose expiration time is before the current time. This function should be
|
func initReplayCache() {
|
||||||
// called periodically to prevent the cache from growing indefinitely.
|
if replayCache == nil {
|
||||||
// It acquires a mutex to ensure thread safety during cleanup.
|
replayCache = NewCache()
|
||||||
// SECURITY FIX: Add proper locking protection for cleanupReplayCache
|
replayCache.SetMaxSize(10000) // Set size limit to 10,000 entries
|
||||||
func cleanupReplayCache() {
|
|
||||||
now := time.Now()
|
|
||||||
// SECURITY FIX: Use safe iteration with proper locking
|
|
||||||
toDelete := make([]string, 0)
|
|
||||||
for token, expiry := range replayCache {
|
|
||||||
if expiry.Before(now) {
|
|
||||||
toDelete = append(toDelete, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Delete expired entries
|
|
||||||
for _, token := range toDelete {
|
|
||||||
delete(replayCache, token)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +123,12 @@ func parseJWT(tokenString string) (*JWT, error) {
|
|||||||
// Parameters:
|
// Parameters:
|
||||||
// - issuerURL: The expected issuer URL (e.g., "https://accounts.google.com").
|
// - issuerURL: The expected issuer URL (e.g., "https://accounts.google.com").
|
||||||
// - clientID: The expected audience value (the client ID of this application).
|
// - clientID: The expected audience value (the client ID of this application).
|
||||||
|
// - skipReplayCheck: If true, skips JTI replay detection (used for revalidation of cached tokens).
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - nil if all standard claims are valid.
|
// - nil if all standard claims are valid.
|
||||||
// - An error describing the first validation failure encountered.
|
// - An error describing the first validation failure encountered.
|
||||||
func (j *JWT) Verify(issuerURL, clientID string) error {
|
func (j *JWT) Verify(issuerURL, clientID string, skipReplayCheck ...bool) error {
|
||||||
// Validate algorithm to prevent algorithm switching attacks
|
// Validate algorithm to prevent algorithm switching attacks
|
||||||
alg, ok := j.Header["alg"].(string)
|
alg, ok := j.Header["alg"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -195,7 +184,10 @@ func (j *JWT) Verify(issuerURL, clientID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implement replay protection by checking the jti (JWT ID)
|
// Implement replay protection by checking the jti (JWT ID)
|
||||||
if jti, ok := claims["jti"].(string); ok {
|
// Skip replay check if explicitly requested (for revalidation scenarios)
|
||||||
|
shouldSkipReplay := len(skipReplayCheck) > 0 && skipReplayCheck[0]
|
||||||
|
|
||||||
|
if jti, ok := claims["jti"].(string); ok && !shouldSkipReplay {
|
||||||
// Skip replay detection for tokens that are being verified from the cache
|
// Skip replay detection for tokens that are being verified from the cache
|
||||||
if j.Token == "" {
|
if j.Token == "" {
|
||||||
// This is a parsed JWT without the original token string,
|
// This is a parsed JWT without the original token string,
|
||||||
@@ -203,15 +195,15 @@ func (j *JWT) Verify(issuerURL, clientID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECURITY FIX: Implement thread-safe replay cache operations with proper locking
|
// SECURITY FIX: Use bounded Cache with thread-safe operations
|
||||||
replayCacheMu.Lock()
|
replayCacheMu.Lock()
|
||||||
defer replayCacheMu.Unlock() // Ensure unlock happens even if panic occurs
|
defer replayCacheMu.Unlock()
|
||||||
|
|
||||||
// SECURITY FIX: Clean up expired entries safely
|
// Initialize cache if not already done
|
||||||
cleanupReplayCache()
|
initReplayCache()
|
||||||
|
|
||||||
// SECURITY FIX: Check for replay attack with atomic operation
|
// SECURITY FIX: Check for replay attack using Cache API
|
||||||
if _, exists := replayCache[jti]; exists {
|
if _, exists := replayCache.Get(jti); exists {
|
||||||
return fmt.Errorf("token replay detected")
|
return fmt.Errorf("token replay detected")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +216,11 @@ func (j *JWT) Verify(issuerURL, clientID string) error {
|
|||||||
expTime = time.Now().Add(10 * time.Minute)
|
expTime = time.Now().Add(10 * time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECURITY FIX: Add to replay cache atomically
|
// SECURITY FIX: Add to replay cache with expiration using Cache API
|
||||||
replayCache[jti] = expTime
|
duration := time.Until(expTime)
|
||||||
|
if duration > 0 {
|
||||||
|
replayCache.Set(jti, true, duration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, ok := claims["sub"].(string)
|
sub, ok := claims["sub"].(string)
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -59,6 +61,33 @@ func createDefaultHTTPClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createTokenHTTPClient creates a specialized HTTP client for token operations.
|
||||||
|
// It reuses the transport from the main HTTP client but adds cookie jar support
|
||||||
|
// and optimized redirect handling for OIDC token endpoints.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - baseClient: The base HTTP client to derive transport settings from.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the configured http.Client optimized for token operations.
|
||||||
|
func createTokenHTTPClient(baseClient *http.Client) *http.Client {
|
||||||
|
// Create a cookie jar for handling redirects with cookies
|
||||||
|
jar, _ := cookiejar.New(nil)
|
||||||
|
|
||||||
|
return &http.Client{
|
||||||
|
Transport: baseClient.Transport, // Reuse the transport from base client
|
||||||
|
Timeout: baseClient.Timeout, // Reuse the timeout from base client
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
// Always follow redirects for OIDC endpoints
|
||||||
|
if len(via) >= 50 {
|
||||||
|
return fmt.Errorf("stopped after 50 redirects")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Jar: jar, // Add cookie jar for redirect handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ConstSessionTimeout = 86400 // Session timeout in seconds
|
ConstSessionTimeout = 86400 // Session timeout in seconds
|
||||||
defaultBlacklistDuration = 24 * time.Hour // Default duration to blacklist a JTI
|
defaultBlacklistDuration = 24 * time.Hour // Default duration to blacklist a JTI
|
||||||
@@ -105,6 +134,7 @@ type TraefikOidc struct {
|
|||||||
scheme string
|
scheme string
|
||||||
tokenCache *TokenCache
|
tokenCache *TokenCache
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
tokenHTTPClient *http.Client // Reusable HTTP client for token operations
|
||||||
logger *Logger
|
logger *Logger
|
||||||
tokenVerifier TokenVerifier
|
tokenVerifier TokenVerifier
|
||||||
jwtVerifier JWTVerifier
|
jwtVerifier JWTVerifier
|
||||||
@@ -124,6 +154,7 @@ type TraefikOidc struct {
|
|||||||
headerTemplates map[string]*template.Template // Parsed templates for custom headers
|
headerTemplates map[string]*template.Template // Parsed templates for custom headers
|
||||||
tokenCleanupStopChan chan struct{} // Channel to stop token cleanup goroutine
|
tokenCleanupStopChan chan struct{} // Channel to stop token cleanup goroutine
|
||||||
metadataRefreshStopChan chan struct{} // Channel to stop metadata refresh goroutine
|
metadataRefreshStopChan chan struct{} // Channel to stop metadata refresh goroutine
|
||||||
|
goroutineWG sync.WaitGroup // WaitGroup to track background goroutines
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderMetadata holds OIDC provider metadata
|
// ProviderMetadata holds OIDC provider metadata
|
||||||
@@ -243,7 +274,14 @@ func (t *TraefikOidc) VerifyToken(token string) error {
|
|||||||
|
|
||||||
// Also update the global replayCache for backwards compatibility
|
// Also update the global replayCache for backwards compatibility
|
||||||
replayCacheMu.Lock()
|
replayCacheMu.Lock()
|
||||||
replayCache[jti] = expiry
|
// Initialize cache if not already done
|
||||||
|
if replayCache == nil {
|
||||||
|
initReplayCache()
|
||||||
|
}
|
||||||
|
duration := time.Until(expiry)
|
||||||
|
if duration > 0 {
|
||||||
|
replayCache.Set(jti, true, duration)
|
||||||
|
}
|
||||||
replayCacheMu.Unlock()
|
replayCacheMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,8 +363,8 @@ func (t *TraefikOidc) VerifyJWTSignatureAndClaims(jwt *JWT, token string) error
|
|||||||
return fmt.Errorf("signature verification failed: %w", err)
|
return fmt.Errorf("signature verification failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify standard claims
|
// Verify standard claims - skip replay check since it's already handled in VerifyToken
|
||||||
if err := jwt.Verify(t.issuerURL, t.clientID); err != nil {
|
if err := jwt.Verify(t.issuerURL, t.clientID, true); err != nil {
|
||||||
return fmt.Errorf("standard claim verification failed: %w", err)
|
return fmt.Errorf("standard claim verification failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +454,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
limiter: rate.NewLimiter(rate.Every(time.Second), config.RateLimit),
|
limiter: rate.NewLimiter(rate.Every(time.Second), config.RateLimit),
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: NewTokenCache(),
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
|
tokenHTTPClient: createTokenHTTPClient(httpClient),
|
||||||
excludedURLs: createStringMap(config.ExcludedURLs),
|
excludedURLs: createStringMap(config.ExcludedURLs),
|
||||||
allowedUserDomains: createStringMap(config.AllowedUserDomains),
|
allowedUserDomains: createStringMap(config.AllowedUserDomains),
|
||||||
allowedUsers: createCaseInsensitiveStringMap(config.AllowedUsers),
|
allowedUsers: createCaseInsensitiveStringMap(config.AllowedUsers),
|
||||||
@@ -526,30 +565,34 @@ func (t *TraefikOidc) updateMetadataEndpoints(metadata *ProviderMetadata) {
|
|||||||
// - providerURL: The base URL of bogged OIDC provider, used for subsequent refresh attempts.
|
// - providerURL: The base URL of bogged OIDC provider, used for subsequent refresh attempts.
|
||||||
func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
||||||
ticker := time.NewTicker(1 * time.Hour)
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
// No defer ticker.Stop() here, it's stopped in the select case
|
t.goroutineWG.Add(1) // Track this goroutine
|
||||||
|
|
||||||
for {
|
go func() {
|
||||||
select {
|
defer t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||||
case <-ticker.C:
|
defer ticker.Stop() // Ensure ticker is always stopped
|
||||||
t.logger.Debug("Refreshing OIDC metadata")
|
|
||||||
metadata, err := t.metadataCache.GetMetadata(providerURL, t.httpClient, t.logger)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("Failed to refresh metadata: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata != nil {
|
for {
|
||||||
t.updateMetadataEndpoints(metadata)
|
select {
|
||||||
t.logger.Debug("Successfully refreshed metadata")
|
case <-ticker.C:
|
||||||
} else {
|
t.logger.Debug("Refreshing OIDC metadata")
|
||||||
t.logger.Error("Received nil metadata during refresh")
|
metadata, err := t.metadataCache.GetMetadata(providerURL, t.httpClient, t.logger)
|
||||||
|
if err != nil {
|
||||||
|
t.logger.Errorf("Failed to refresh metadata: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata != nil {
|
||||||
|
t.updateMetadataEndpoints(metadata)
|
||||||
|
t.logger.Debug("Successfully refreshed metadata")
|
||||||
|
} else {
|
||||||
|
t.logger.Error("Received nil metadata during refresh")
|
||||||
|
}
|
||||||
|
case <-t.metadataRefreshStopChan:
|
||||||
|
t.logger.Debug("Metadata refresh goroutine stopped.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
case <-t.metadataRefreshStopChan:
|
|
||||||
ticker.Stop()
|
|
||||||
t.logger.Debug("Metadata refresh goroutine stopped.")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// discoverProviderMetadata attempts to fetch the OIDC provider's configuration from its
|
// discoverProviderMetadata attempts to fetch the OIDC provider's configuration from its
|
||||||
@@ -1720,8 +1763,11 @@ func (t *TraefikOidc) validateHost(host string) error {
|
|||||||
// the token cache, token blacklist cache, and JWK cache.
|
// the token cache, token blacklist cache, and JWK cache.
|
||||||
func (t *TraefikOidc) startTokenCleanup() {
|
func (t *TraefikOidc) startTokenCleanup() {
|
||||||
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
|
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
|
||||||
|
t.goroutineWG.Add(1) // Track this goroutine
|
||||||
go func() {
|
go func() {
|
||||||
// No defer ticker.Stop() here, it's stopped in the select case
|
defer t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||||
|
defer ticker.Stop() // Ensure ticker is always stopped
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@@ -1741,7 +1787,6 @@ func (t *TraefikOidc) startTokenCleanup() {
|
|||||||
t.jwkCache.Cleanup()
|
t.jwkCache.Cleanup()
|
||||||
}
|
}
|
||||||
case <-t.tokenCleanupStopChan:
|
case <-t.tokenCleanupStopChan:
|
||||||
ticker.Stop()
|
|
||||||
t.logger.Debug("Token cleanup goroutine stopped.")
|
t.logger.Debug("Token cleanup goroutine stopped.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2231,20 +2276,36 @@ func (t *TraefikOidc) sendErrorResponse(rw http.ResponseWriter, req *http.Reques
|
|||||||
_, _ = rw.Write([]byte(htmlBody)) // Ignore write error as header is already sent
|
_, _ = rw.Write([]byte(htmlBody)) // Ignore write error as header is already sent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close stops all background goroutines and closes resources.
|
// Close stops all background goroutines and closes resources with proper timeout.
|
||||||
func (t *TraefikOidc) Close() error {
|
func (t *TraefikOidc) Close() error {
|
||||||
t.logger.Debug("Closing TraefikOidc plugin instance")
|
t.logger.Debug("Closing TraefikOidc plugin instance")
|
||||||
// Signal and close tokenCleanupStopChan
|
|
||||||
|
// Signal all goroutines to stop
|
||||||
if t.tokenCleanupStopChan != nil {
|
if t.tokenCleanupStopChan != nil {
|
||||||
close(t.tokenCleanupStopChan)
|
close(t.tokenCleanupStopChan)
|
||||||
t.logger.Debug("tokenCleanupStopChan closed")
|
t.logger.Debug("tokenCleanupStopChan closed")
|
||||||
}
|
}
|
||||||
// Signal and close metadataRefreshStopChan
|
|
||||||
if t.metadataRefreshStopChan != nil {
|
if t.metadataRefreshStopChan != nil {
|
||||||
close(t.metadataRefreshStopChan)
|
close(t.metadataRefreshStopChan)
|
||||||
t.logger.Debug("metadataRefreshStopChan closed")
|
t.logger.Debug("metadataRefreshStopChan closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for all goroutines to finish with timeout
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
t.goroutineWG.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for goroutines to finish or timeout after 10 seconds
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
t.logger.Debug("All background goroutines stopped gracefully")
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.logger.Errorf("Timeout waiting for background goroutines to stop")
|
||||||
|
// Continue with cleanup even if goroutines didn't stop gracefully
|
||||||
|
}
|
||||||
|
|
||||||
// Close caches
|
// Close caches
|
||||||
// These Close methods should stop their respective autoCleanupRoutine goroutines
|
// These Close methods should stop their respective autoCleanupRoutine goroutines
|
||||||
if t.tokenBlacklist != nil {
|
if t.tokenBlacklist != nil {
|
||||||
|
|||||||
+764
-3
@@ -722,7 +722,8 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
|
|
||||||
// Reset the global replayCache to prevent "token replay detected" errors
|
// Reset the global replayCache to prevent "token replay detected" errors
|
||||||
replayCacheMu.Lock()
|
replayCacheMu.Lock()
|
||||||
replayCache = make(map[string]time.Time) // Reset the global cache
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
replayCacheMu.Unlock()
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
// Store original tokenVerifier to restore later
|
// Store original tokenVerifier to restore later
|
||||||
@@ -734,7 +735,8 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
VerifyFunc: func(token string) error {
|
VerifyFunc: func(token string) error {
|
||||||
// Clear replay cache before token verification
|
// Clear replay cache before token verification
|
||||||
replayCacheMu.Lock()
|
replayCacheMu.Lock()
|
||||||
replayCache = make(map[string]time.Time)
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
replayCacheMu.Unlock()
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
// Call the original verifier's VerifyToken method
|
// Call the original verifier's VerifyToken method
|
||||||
@@ -1143,7 +1145,8 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
// Clear the global replay cache before each test run
|
// Clear the global replay cache before each test run
|
||||||
replayCacheMu.Lock()
|
replayCacheMu.Lock()
|
||||||
replayCache = make(map[string]time.Time) // Reset the global cache
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
replayCacheMu.Unlock()
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
// Explicitly clear the shared blacklist at the start of each sub-test
|
// Explicitly clear the shared blacklist at the start of each sub-test
|
||||||
@@ -2803,3 +2806,761 @@ func TestVerifyTimeConstraint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} // Add missing closing brace for TestVerifyTimeConstraint
|
} // Add missing closing brace for TestVerifyTimeConstraint
|
||||||
|
|
||||||
|
// ===== JWT REPLAY DETECTION TESTS =====
|
||||||
|
// These tests ensure the replay detection fix works correctly and prevents regressions
|
||||||
|
|
||||||
|
// TestJWTVerifyWithSkipReplayCheck tests the new skipReplayCheck parameter functionality
|
||||||
|
func TestJWTVerifyWithSkipReplayCheck(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Clear the global replay cache before test
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Create a test JWT with unique JTI
|
||||||
|
jti := generateRandomString(16)
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": jti,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
skipReplayCheck bool
|
||||||
|
firstCall bool
|
||||||
|
expectError bool
|
||||||
|
errorContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "First verification with skipReplayCheck=false should succeed",
|
||||||
|
skipReplayCheck: false,
|
||||||
|
firstCall: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Second verification with skipReplayCheck=false should fail (replay detected)",
|
||||||
|
skipReplayCheck: false,
|
||||||
|
firstCall: false,
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "token replay detected",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Verification with skipReplayCheck=true should always succeed",
|
||||||
|
skipReplayCheck: true,
|
||||||
|
firstCall: false, // Even on subsequent calls
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.firstCall {
|
||||||
|
// Clear replay cache for first call tests
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := jwt.Verify("https://test-issuer.com", "test-client-id", tc.skipReplayCheck)
|
||||||
|
|
||||||
|
if tc.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error containing '%s', but got nil", tc.errorContains)
|
||||||
|
} else if !strings.Contains(err.Error(), tc.errorContains) {
|
||||||
|
t.Errorf("Expected error containing '%s', got '%v'", tc.errorContains, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJWTVerifyBackwardCompatibility tests that calls without the skipReplayCheck parameter default to replay checking
|
||||||
|
func TestJWTVerifyBackwardCompatibility(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Clear the global replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Create a test JWT with unique JTI
|
||||||
|
jti := generateRandomString(16)
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": jti,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First call with old signature (no skipReplayCheck parameter) should succeed
|
||||||
|
err = jwt.Verify("https://test-issuer.com", "test-client-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("First verification should succeed, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second call with old signature should fail due to replay detection
|
||||||
|
err = jwt.Verify("https://test-issuer.com", "test-client-id")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Second verification should fail due to replay detection")
|
||||||
|
} else if !strings.Contains(err.Error(), "token replay detected") {
|
||||||
|
t.Errorf("Expected 'token replay detected' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenReplayDetectionFalsePositiveFix tests the specific scenario that was causing false positives
|
||||||
|
func TestTokenReplayDetectionFalsePositiveFix(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Clear the global replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Create a test JWT with unique JTI
|
||||||
|
jti := generateRandomString(16)
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": jti,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate the authentication flow that was causing false positives:
|
||||||
|
// 1. Initial authentication adds JTI to cache
|
||||||
|
// 2. Subsequent request validation should not trigger false positive
|
||||||
|
|
||||||
|
// Step 1: Initial authentication (this would add JTI to cache)
|
||||||
|
jwt1, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT for initial auth: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = jwt1.Verify("https://test-issuer.com", "test-client-id", false) // Normal replay check
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Initial authentication should succeed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Subsequent request validation (this should skip replay check to avoid false positive)
|
||||||
|
jwt2, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT for subsequent request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = jwt2.Verify("https://test-issuer.com", "test-client-id", true) // Skip replay check
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Subsequent request validation should succeed with skipReplayCheck=true: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Verify that actual replay attacks are still detected
|
||||||
|
jwt3, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT for replay attack test: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = jwt3.Verify("https://test-issuer.com", "test-client-id", false) // Normal replay check
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Actual replay attack should be detected when skipReplayCheck=false")
|
||||||
|
} else if !strings.Contains(err.Error(), "token replay detected") {
|
||||||
|
t.Errorf("Expected 'token replay detected' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuthenticationFlowReplayDetection tests the complete authentication flow
|
||||||
|
func TestAuthenticationFlowReplayDetection(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Clear the global replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Create a test JWT with unique JTI
|
||||||
|
jti := generateRandomString(16)
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": jti,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the complete flow:
|
||||||
|
// 1. Initial authentication (should add JTI to cache)
|
||||||
|
// 2. Multiple subsequent requests (should not trigger false positives)
|
||||||
|
// 3. Actual replay attack from different source (should be detected)
|
||||||
|
|
||||||
|
// Step 1: Initial authentication
|
||||||
|
err = ts.tOidc.VerifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Initial authentication should succeed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify JTI is in cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
_, exists := replayCache.Get(jti)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
if !exists {
|
||||||
|
t.Error("JTI should be added to replay cache during initial authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Subsequent requests (simulate normal request processing)
|
||||||
|
// These should use the token cache and skip replay detection
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
err = ts.tOidc.VerifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Subsequent request %d should succeed: %v", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Simulate actual replay attack by directly calling JWT.Verify with replay check
|
||||||
|
jwt, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT for replay attack test: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = jwt.Verify("https://test-issuer.com", "test-client-id", false) // Force replay check
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Actual replay attack should be detected")
|
||||||
|
} else if !strings.Contains(err.Error(), "token replay detected") {
|
||||||
|
t.Errorf("Expected 'token replay detected' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestActualReplayAttackDetection ensures real replay attacks are still properly detected
|
||||||
|
func TestActualReplayAttackDetection(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Clear the global replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Create a test JWT with unique JTI
|
||||||
|
jti := generateRandomString(16)
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": jti,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First verification should succeed
|
||||||
|
err = jwt.Verify("https://test-issuer.com", "test-client-id", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("First verification should succeed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate different types of replay attacks
|
||||||
|
replayTests := []struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Direct replay attack",
|
||||||
|
description: "Same token used again with replay checking enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Replay from different source",
|
||||||
|
description: "Token intercepted and replayed by attacker",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rt := range replayTests {
|
||||||
|
t.Run(rt.name, func(t *testing.T) {
|
||||||
|
// Parse token again (simulating replay)
|
||||||
|
replayJWT, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT for replay test: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt replay with normal replay checking
|
||||||
|
err = replayJWT.Verify("https://test-issuer.com", "test-client-id", false)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Replay attack should be detected for: %s", rt.description)
|
||||||
|
} else if !strings.Contains(err.Error(), "token replay detected") {
|
||||||
|
t.Errorf("Expected 'token replay detected' error for %s, got: %v", rt.description, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConcurrentTokenValidation tests thread safety of replay detection
|
||||||
|
func TestConcurrentTokenValidation(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure rate limiter to allow more requests for concurrent testing
|
||||||
|
ts.tOidc.limiter = rate.NewLimiter(rate.Limit(1000), 1000) // Allow 1000 requests per second with burst of 1000
|
||||||
|
|
||||||
|
// Clear the global replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Create multiple tokens with unique JTIs
|
||||||
|
var tokens []string
|
||||||
|
var jtis []string
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
jti := generateRandomString(16)
|
||||||
|
jtis = append(jtis, jti)
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": jti,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT %d: %v", i, err)
|
||||||
|
}
|
||||||
|
tokens = append(tokens, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test concurrent validation
|
||||||
|
const numGoroutines = 20
|
||||||
|
const numIterations = 5
|
||||||
|
|
||||||
|
results := make(chan error, numGoroutines*numIterations)
|
||||||
|
|
||||||
|
for g := 0; g < numGoroutines; g++ {
|
||||||
|
go func(goroutineID int) {
|
||||||
|
for i := 0; i < numIterations; i++ {
|
||||||
|
tokenIndex := (goroutineID + i) % len(tokens)
|
||||||
|
token := tokens[tokenIndex]
|
||||||
|
|
||||||
|
// First validation should succeed
|
||||||
|
err := ts.tOidc.VerifyToken(token)
|
||||||
|
results <- err
|
||||||
|
|
||||||
|
// Subsequent validation with same token should also succeed (uses cache)
|
||||||
|
err = ts.tOidc.VerifyToken(token)
|
||||||
|
results <- err
|
||||||
|
}
|
||||||
|
}(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
var errors []error
|
||||||
|
for i := 0; i < numGoroutines*numIterations*2; i++ {
|
||||||
|
if err := <-results; err != nil {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All validations should succeed (no race conditions)
|
||||||
|
if len(errors) > 0 {
|
||||||
|
t.Errorf("Expected no errors in concurrent validation, got %d errors: %v", len(errors), errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all JTIs are in cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
for i, jti := range jtis {
|
||||||
|
if _, exists := replayCache.Get(jti); !exists {
|
||||||
|
t.Errorf("JTI %d (%s) should be in replay cache", i, jti)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJTIBlacklistBehavior tests the JTI blacklist cache management
|
||||||
|
func TestJTIBlacklistBehavior(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Clear the global replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Create a test JWT with unique JTI
|
||||||
|
jti := generateRandomString(16)
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": jti,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test JTI blacklist behavior
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
action func() error
|
||||||
|
expectError bool
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Initial verification adds JTI to blacklist",
|
||||||
|
action: func() error {
|
||||||
|
return ts.tOidc.VerifyToken(token)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
description: "First verification should succeed and add JTI to blacklist",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JTI exists in blacklist after verification",
|
||||||
|
action: func() error {
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
defer replayCacheMu.Unlock()
|
||||||
|
if _, exists := replayCache.Get(jti); !exists {
|
||||||
|
return fmt.Errorf("JTI not found in blacklist cache")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
description: "JTI should be present in blacklist cache",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Subsequent verification uses cache (no replay check)",
|
||||||
|
action: func() error {
|
||||||
|
return ts.tOidc.VerifyToken(token)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
description: "Subsequent verification should succeed using token cache",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Direct JWT verification detects replay",
|
||||||
|
action: func() error {
|
||||||
|
jwt, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return jwt.Verify("https://test-issuer.com", "test-client-id", false)
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
description: "Direct JWT verification should detect replay",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.action()
|
||||||
|
|
||||||
|
if tc.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error for %s, but got nil", tc.description)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error for %s, but got: %v", tc.description, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSessionBasedTokenRevalidation tests token revalidation in session-based scenarios
|
||||||
|
func TestSessionBasedTokenRevalidation(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Clear the global replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
// Create a test JWT with unique JTI
|
||||||
|
jti := generateRandomString(16)
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": jti,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate session-based token revalidation scenario
|
||||||
|
// This tests the specific case that was causing false positives
|
||||||
|
|
||||||
|
// Step 1: Initial authentication (callback processing)
|
||||||
|
err = ts.tOidc.VerifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Initial authentication should succeed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Multiple session-based requests (normal request processing)
|
||||||
|
// These should not trigger replay detection false positives
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
err = ts.tOidc.VerifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Session request %d should succeed: %v", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Verify token is in both caches appropriately
|
||||||
|
// Check token cache
|
||||||
|
if _, exists := ts.tOidc.tokenCache.Get(token); !exists {
|
||||||
|
t.Error("Token should be in token cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
_, inReplayCache := replayCache.Get(jti)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
if !inReplayCache {
|
||||||
|
t.Error("JTI should be in replay cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Verify that clearing token cache still allows validation
|
||||||
|
ts.tOidc.tokenCache = NewTokenCache() // Clear token cache
|
||||||
|
|
||||||
|
err = ts.tOidc.VerifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Token validation should succeed even after cache clear: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEdgeCasesWithDifferentTokenTypes tests replay detection with different token types
|
||||||
|
func TestEdgeCasesWithDifferentTokenTypes(t *testing.T) {
|
||||||
|
ts := &TestSuite{t: t}
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Clear the global replay cache
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
replayCache = NewCache()
|
||||||
|
replayCache.SetMaxSize(10000)
|
||||||
|
replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
iat := now.Unix()
|
||||||
|
nbf := now.Unix()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tokenType string
|
||||||
|
claims map[string]interface{}
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ID Token with JTI",
|
||||||
|
tokenType: "id_token",
|
||||||
|
claims: map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
"jti": generateRandomString(16),
|
||||||
|
"token_type": "id_token",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Access Token with JTI",
|
||||||
|
tokenType: "access_token",
|
||||||
|
claims: map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"scope": "openid profile email",
|
||||||
|
"jti": generateRandomString(16),
|
||||||
|
"token_type": "access_token",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Token without JTI",
|
||||||
|
tokenType: "no_jti",
|
||||||
|
claims: map[string]interface{}{
|
||||||
|
"iss": "https://test-issuer.com",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"nbf": nbf,
|
||||||
|
"sub": "test-subject",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"nonce": "test-nonce",
|
||||||
|
// No JTI claim
|
||||||
|
},
|
||||||
|
expectError: false, // Should still work, just no replay protection
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Create token with specific claims
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", tc.claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First verification should succeed
|
||||||
|
err = ts.tOidc.VerifyToken(token)
|
||||||
|
if tc.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error for token type %s, but got nil", tc.tokenType)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error for token type %s, but got: %v", tc.tokenType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second verification should also succeed (uses cache)
|
||||||
|
if !tc.expectError {
|
||||||
|
err = ts.tOidc.VerifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Second verification should succeed for token type %s: %v", tc.tokenType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test direct JWT verification for replay detection
|
||||||
|
if !tc.expectError && tc.claims["jti"] != nil {
|
||||||
|
jwt, err := parseJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should detect replay for tokens with JTI
|
||||||
|
err = jwt.Verify("https://test-issuer.com", "test-client-id", false)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected replay detection for token type %s with JTI", tc.tokenType)
|
||||||
|
} else if !strings.Contains(err.Error(), "token replay detected") {
|
||||||
|
t.Errorf("Expected 'token replay detected' error for token type %s, got: %v", tc.tokenType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ type PerformanceMetrics struct {
|
|||||||
// Resource metrics
|
// Resource metrics
|
||||||
memoryUsage int64
|
memoryUsage int64
|
||||||
goroutineCount int64
|
goroutineCount int64
|
||||||
|
memoryPressure int64 // Memory pressure level (0-100)
|
||||||
|
gcPauseTime int64 // Last GC pause time in nanoseconds
|
||||||
|
heapSize int64 // Current heap size
|
||||||
|
heapInUse int64 // Heap memory in use
|
||||||
|
|
||||||
// Error metrics (kept for backward compatibility)
|
// Error metrics (kept for backward compatibility)
|
||||||
verificationErrors int64
|
verificationErrors int64
|
||||||
@@ -251,9 +255,36 @@ func (pm *PerformanceMetrics) collectSystemMetrics() {
|
|||||||
var m runtime.MemStats
|
var m runtime.MemStats
|
||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
atomic.StoreInt64(&pm.memoryUsage, int64(m.Alloc))
|
atomic.StoreInt64(&pm.memoryUsage, int64(m.Alloc))
|
||||||
|
atomic.StoreInt64(&pm.heapSize, int64(m.HeapSys))
|
||||||
|
atomic.StoreInt64(&pm.heapInUse, int64(m.HeapInuse))
|
||||||
|
atomic.StoreInt64(&pm.gcPauseTime, int64(m.PauseNs[(m.NumGC+255)%256]))
|
||||||
|
|
||||||
|
// Calculate memory pressure (0-100 scale)
|
||||||
|
// Based on heap utilization and GC frequency
|
||||||
|
heapUtilization := float64(m.HeapInuse) / float64(m.HeapSys)
|
||||||
|
gcFrequency := float64(m.NumGC) / time.Since(pm.startTime).Minutes()
|
||||||
|
|
||||||
|
// Memory pressure calculation
|
||||||
|
pressure := int64(heapUtilization * 50) // 0-50 based on heap utilization
|
||||||
|
if gcFrequency > 10 { // High GC frequency indicates pressure
|
||||||
|
pressure += int64((gcFrequency - 10) * 2) // Add up to 50 more
|
||||||
|
}
|
||||||
|
if pressure > 100 {
|
||||||
|
pressure = 100
|
||||||
|
}
|
||||||
|
atomic.StoreInt64(&pm.memoryPressure, pressure)
|
||||||
|
|
||||||
// Goroutine count
|
// Goroutine count
|
||||||
atomic.StoreInt64(&pm.goroutineCount, int64(runtime.NumGoroutine()))
|
atomic.StoreInt64(&pm.goroutineCount, int64(runtime.NumGoroutine()))
|
||||||
|
|
||||||
|
// Log memory pressure warnings
|
||||||
|
if pressure > 80 {
|
||||||
|
pm.logger.Errorf("High memory pressure detected: %d%% (heap utilization: %.1f%%, GC frequency: %.1f/min)",
|
||||||
|
pressure, heapUtilization*100, gcFrequency)
|
||||||
|
} else if pressure > 60 {
|
||||||
|
pm.logger.Infof("Moderate memory pressure: %d%% (heap utilization: %.1f%%, GC frequency: %.1f/min)",
|
||||||
|
pressure, heapUtilization*100, gcFrequency)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMetrics returns all current performance metrics
|
// GetMetrics returns all current performance metrics
|
||||||
@@ -317,6 +348,10 @@ func (pm *PerformanceMetrics) GetMetrics() map[string]interface{} {
|
|||||||
|
|
||||||
// Resource metrics
|
// Resource metrics
|
||||||
"memory_usage_bytes": atomic.LoadInt64(&pm.memoryUsage),
|
"memory_usage_bytes": atomic.LoadInt64(&pm.memoryUsage),
|
||||||
|
"memory_pressure": atomic.LoadInt64(&pm.memoryPressure),
|
||||||
|
"heap_size_bytes": atomic.LoadInt64(&pm.heapSize),
|
||||||
|
"heap_inuse_bytes": atomic.LoadInt64(&pm.heapInUse),
|
||||||
|
"gc_pause_time_ns": atomic.LoadInt64(&pm.gcPauseTime),
|
||||||
"goroutine_count": atomic.LoadInt64(&pm.goroutineCount),
|
"goroutine_count": atomic.LoadInt64(&pm.goroutineCount),
|
||||||
|
|
||||||
// Rate limiting metrics
|
// Rate limiting metrics
|
||||||
@@ -414,6 +449,10 @@ type ResourceMonitor struct {
|
|||||||
// Session limits
|
// Session limits
|
||||||
maxSessions int64
|
maxSessions int64
|
||||||
|
|
||||||
|
// Cache size tracking
|
||||||
|
cacheSizes map[string]int64
|
||||||
|
cacheMutex sync.RWMutex
|
||||||
|
|
||||||
// Monitoring state
|
// Monitoring state
|
||||||
alertThresholds map[string]float64
|
alertThresholds map[string]float64
|
||||||
alerts []ResourceAlert
|
alerts []ResourceAlert
|
||||||
@@ -441,11 +480,13 @@ func NewResourceMonitor(perfMetrics *PerformanceMetrics, logger *Logger) *Resour
|
|||||||
maxMemoryBytes: 100 * 1024 * 1024, // 100MB default
|
maxMemoryBytes: 100 * 1024 * 1024, // 100MB default
|
||||||
maxCacheSize: 10000, // 10k items default
|
maxCacheSize: 10000, // 10k items default
|
||||||
maxSessions: 1000, // 1k sessions default
|
maxSessions: 1000, // 1k sessions default
|
||||||
|
cacheSizes: make(map[string]int64),
|
||||||
alertThresholds: map[string]float64{
|
alertThresholds: map[string]float64{
|
||||||
"memory_usage": 0.8, // 80%
|
"memory_usage": 0.8, // 80%
|
||||||
"cache_usage": 0.9, // 90%
|
"memory_pressure": 0.7, // 70%
|
||||||
"session_usage": 0.85, // 85%
|
"cache_usage": 0.9, // 90%
|
||||||
"error_rate": 0.1, // 10%
|
"session_usage": 0.85, // 85%
|
||||||
|
"error_rate": 0.1, // 10%
|
||||||
},
|
},
|
||||||
alerts: make([]ResourceAlert, 0),
|
alerts: make([]ResourceAlert, 0),
|
||||||
perfMetrics: perfMetrics,
|
perfMetrics: perfMetrics,
|
||||||
@@ -473,6 +514,25 @@ func (rm *ResourceMonitor) SetSessionLimit(count int64) {
|
|||||||
rm.maxSessions = count
|
rm.maxSessions = count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateCacheSize updates the size of a specific cache
|
||||||
|
func (rm *ResourceMonitor) UpdateCacheSize(cacheName string, size int64) {
|
||||||
|
rm.cacheMutex.Lock()
|
||||||
|
defer rm.cacheMutex.Unlock()
|
||||||
|
rm.cacheSizes[cacheName] = size
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCacheSizes returns current cache sizes
|
||||||
|
func (rm *ResourceMonitor) GetCacheSizes() map[string]int64 {
|
||||||
|
rm.cacheMutex.RLock()
|
||||||
|
defer rm.cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
sizes := make(map[string]int64)
|
||||||
|
for name, size := range rm.cacheSizes {
|
||||||
|
sizes[name] = size
|
||||||
|
}
|
||||||
|
return sizes
|
||||||
|
}
|
||||||
|
|
||||||
// startMonitoring starts the background monitoring routine
|
// startMonitoring starts the background monitoring routine
|
||||||
func (rm *ResourceMonitor) startMonitoring() {
|
func (rm *ResourceMonitor) startMonitoring() {
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
@@ -502,6 +562,21 @@ func (rm *ResourceMonitor) checkResourceUsage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check memory pressure
|
||||||
|
if memPressure, ok := metrics["memory_pressure"].(int64); ok {
|
||||||
|
pressureRatio := float64(memPressure) / 100.0 // Convert to 0-1 scale
|
||||||
|
if pressureRatio > rm.alertThresholds["memory_pressure"] {
|
||||||
|
rm.addAlert(ResourceAlert{
|
||||||
|
Type: "memory_pressure",
|
||||||
|
Message: "Memory pressure exceeds threshold",
|
||||||
|
Threshold: rm.alertThresholds["memory_pressure"],
|
||||||
|
CurrentValue: pressureRatio,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Severity: rm.getSeverity(pressureRatio, rm.alertThresholds["memory_pressure"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check cache usage
|
// Check cache usage
|
||||||
if cacheSize, ok := metrics["cache_size"].(int64); ok {
|
if cacheSize, ok := metrics["cache_size"].(int64); ok {
|
||||||
cacheUsageRatio := float64(cacheSize) / float64(rm.maxCacheSize)
|
cacheUsageRatio := float64(cacheSize) / float64(rm.maxCacheSize)
|
||||||
@@ -592,6 +667,7 @@ func (rm *ResourceMonitor) GetAlerts() []ResourceAlert {
|
|||||||
// GetResourceStatus returns current resource status
|
// GetResourceStatus returns current resource status
|
||||||
func (rm *ResourceMonitor) GetResourceStatus() map[string]interface{} {
|
func (rm *ResourceMonitor) GetResourceStatus() map[string]interface{} {
|
||||||
metrics := rm.perfMetrics.GetMetrics()
|
metrics := rm.perfMetrics.GetMetrics()
|
||||||
|
cacheSizes := rm.GetCacheSizes()
|
||||||
|
|
||||||
status := map[string]interface{}{
|
status := map[string]interface{}{
|
||||||
"limits": map[string]interface{}{
|
"limits": map[string]interface{}{
|
||||||
@@ -599,8 +675,9 @@ func (rm *ResourceMonitor) GetResourceStatus() map[string]interface{} {
|
|||||||
"max_cache_size": rm.maxCacheSize,
|
"max_cache_size": rm.maxCacheSize,
|
||||||
"max_sessions": rm.maxSessions,
|
"max_sessions": rm.maxSessions,
|
||||||
},
|
},
|
||||||
"thresholds": rm.alertThresholds,
|
"thresholds": rm.alertThresholds,
|
||||||
"current": metrics,
|
"current": metrics,
|
||||||
|
"cache_sizes": cacheSizes,
|
||||||
// Add expected keys for tests
|
// Add expected keys for tests
|
||||||
"memory_limit": uint64(rm.maxMemoryBytes),
|
"memory_limit": uint64(rm.maxMemoryBytes),
|
||||||
"cache_limit": int(rm.maxCacheSize),
|
"cache_limit": int(rm.maxCacheSize),
|
||||||
@@ -611,6 +688,9 @@ func (rm *ResourceMonitor) GetResourceStatus() map[string]interface{} {
|
|||||||
if memUsage, ok := metrics["memory_usage_bytes"].(int64); ok {
|
if memUsage, ok := metrics["memory_usage_bytes"].(int64); ok {
|
||||||
status["memory_usage_ratio"] = float64(memUsage) / float64(rm.maxMemoryBytes)
|
status["memory_usage_ratio"] = float64(memUsage) / float64(rm.maxMemoryBytes)
|
||||||
}
|
}
|
||||||
|
if memPressure, ok := metrics["memory_pressure"].(int64); ok {
|
||||||
|
status["memory_pressure_ratio"] = float64(memPressure) / 100.0
|
||||||
|
}
|
||||||
if cacheSize, ok := metrics["cache_size"].(int64); ok {
|
if cacheSize, ok := metrics["cache_size"].(int64); ok {
|
||||||
status["cache_usage_ratio"] = float64(cacheSize) / float64(rm.maxCacheSize)
|
status["cache_usage_ratio"] = float64(cacheSize) / float64(rm.maxCacheSize)
|
||||||
}
|
}
|
||||||
@@ -618,5 +698,12 @@ func (rm *ResourceMonitor) GetResourceStatus() map[string]interface{} {
|
|||||||
status["session_usage_ratio"] = float64(activeSessions) / float64(rm.maxSessions)
|
status["session_usage_ratio"] = float64(activeSessions) / float64(rm.maxSessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate total cache size across all caches
|
||||||
|
var totalCacheSize int64
|
||||||
|
for _, size := range cacheSizes {
|
||||||
|
totalCacheSize += size
|
||||||
|
}
|
||||||
|
status["total_cache_size"] = totalCacheSize
|
||||||
|
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-5
@@ -190,13 +190,18 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*
|
|||||||
// Initialize session pool.
|
// Initialize session pool.
|
||||||
sm.sessionPool.New = func() interface{} {
|
sm.sessionPool.New = func() interface{} {
|
||||||
// Initialize SessionData with necessary fields and the mutex.
|
// Initialize SessionData with necessary fields and the mutex.
|
||||||
return &SessionData{
|
sd := &SessionData{
|
||||||
manager: sm,
|
manager: sm,
|
||||||
accessTokenChunks: make(map[int]*sessions.Session),
|
accessTokenChunks: make(map[int]*sessions.Session),
|
||||||
refreshTokenChunks: make(map[int]*sessions.Session),
|
refreshTokenChunks: make(map[int]*sessions.Session),
|
||||||
refreshMutex: sync.Mutex{}, // Initialize the mutex
|
refreshMutex: sync.Mutex{}, // Initialize the mutex
|
||||||
dirty: false, // Initialize dirty flag
|
sessionMutex: sync.RWMutex{}, // Initialize the session mutex
|
||||||
|
dirty: false, // Initialize dirty flag
|
||||||
|
inUse: false, // Initialize in-use flag
|
||||||
}
|
}
|
||||||
|
// Ensure the object is properly reset when created
|
||||||
|
sd.Reset()
|
||||||
|
return sd
|
||||||
}
|
}
|
||||||
|
|
||||||
return sm, nil
|
return sm, nil
|
||||||
@@ -482,6 +487,8 @@ func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
|||||||
// STABILITY FIX: Mark as not in use and return session to pool, regardless of error.
|
// STABILITY FIX: Mark as not in use and return session to pool, regardless of error.
|
||||||
// This ensures the session is always returned to the pool, preventing memory leaks.
|
// This ensures the session is always returned to the pool, preventing memory leaks.
|
||||||
sd.inUse = false
|
sd.inUse = false
|
||||||
|
// Reset the session data before returning to pool to prevent data leakage
|
||||||
|
sd.Reset()
|
||||||
sd.manager.sessionPool.Put(sd)
|
sd.manager.sessionPool.Put(sd)
|
||||||
|
|
||||||
// Return the error from Save, if any
|
// Return the error from Save, if any
|
||||||
@@ -602,6 +609,52 @@ func (sd *SessionData) SetAuthenticated(value bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset clears all session data and prepares the SessionData object for reuse.
|
||||||
|
// This method is called when returning objects to the pool to prevent data leakage
|
||||||
|
// between different users/sessions.
|
||||||
|
func (sd *SessionData) Reset() {
|
||||||
|
sd.sessionMutex.Lock()
|
||||||
|
defer sd.sessionMutex.Unlock()
|
||||||
|
|
||||||
|
// Clear all session values if sessions exist
|
||||||
|
if sd.mainSession != nil {
|
||||||
|
for k := range sd.mainSession.Values {
|
||||||
|
delete(sd.mainSession.Values, k)
|
||||||
|
}
|
||||||
|
sd.mainSession.ID = ""
|
||||||
|
sd.mainSession.IsNew = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if sd.accessSession != nil {
|
||||||
|
for k := range sd.accessSession.Values {
|
||||||
|
delete(sd.accessSession.Values, k)
|
||||||
|
}
|
||||||
|
sd.accessSession.ID = ""
|
||||||
|
sd.accessSession.IsNew = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if sd.refreshSession != nil {
|
||||||
|
for k := range sd.refreshSession.Values {
|
||||||
|
delete(sd.refreshSession.Values, k)
|
||||||
|
}
|
||||||
|
sd.refreshSession.ID = ""
|
||||||
|
sd.refreshSession.IsNew = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear chunk maps
|
||||||
|
for k := range sd.accessTokenChunks {
|
||||||
|
delete(sd.accessTokenChunks, k)
|
||||||
|
}
|
||||||
|
for k := range sd.refreshTokenChunks {
|
||||||
|
delete(sd.refreshTokenChunks, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state flags
|
||||||
|
sd.dirty = false
|
||||||
|
sd.inUse = false
|
||||||
|
sd.request = nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReturnToPool explicitly returns this SessionData object to the pool.
|
// ReturnToPool explicitly returns this SessionData object to the pool.
|
||||||
// This should be called when you're done with a SessionData in any error path
|
// This should be called when you're done with a SessionData in any error path
|
||||||
// where Clear() is not called, to prevent memory leaks.
|
// where Clear() is not called, to prevent memory leaks.
|
||||||
@@ -609,8 +662,8 @@ func (sd *SessionData) ReturnToPool() {
|
|||||||
if sd != nil && sd.manager != nil {
|
if sd != nil && sd.manager != nil {
|
||||||
// STABILITY FIX: Only return to pool if not currently in use
|
// STABILITY FIX: Only return to pool if not currently in use
|
||||||
if !sd.inUse {
|
if !sd.inUse {
|
||||||
// Clear request reference to avoid memory leaks
|
// Reset the session data before returning to pool
|
||||||
sd.request = nil
|
sd.Reset()
|
||||||
sd.manager.sessionPool.Put(sd)
|
sd.manager.sessionPool.Put(sd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user