mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-19 04:47:48 +00:00
fixup! Simplify all the things.
This commit is contained in:
+17
-33
@@ -162,19 +162,11 @@ func TestAzureOIDCRegression(t *testing.T) {
|
|||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetEmail("[email protected]")
|
||||||
|
|
||||||
// Create a valid JWT access token for testing
|
// Use standardized test tokens with valid future expiration dates
|
||||||
accessTokenClaims := map[string]interface{}{
|
accessToken := ValidAccessToken // This token expires in 2065
|
||||||
"iss": "https://login.microsoftonline.com/tenant-id/v2.0",
|
|
||||||
"aud": "test-client-id",
|
|
||||||
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
|
||||||
"iat": time.Now().Unix(),
|
|
||||||
"sub": "user123",
|
|
||||||
"email": "[email protected]",
|
|
||||||
}
|
|
||||||
accessToken, _ := createMockJWT(accessTokenClaims)
|
|
||||||
session.SetAccessToken(accessToken)
|
session.SetAccessToken(accessToken)
|
||||||
|
|
||||||
// Create an invalid/expired ID token
|
// Create an expired ID token using a mock JWT with past expiration
|
||||||
idTokenClaims := map[string]interface{}{
|
idTokenClaims := map[string]interface{}{
|
||||||
"iss": "https://login.microsoftonline.com/tenant-id/v2.0",
|
"iss": "https://login.microsoftonline.com/tenant-id/v2.0",
|
||||||
"aud": "test-client-id",
|
"aud": "test-client-id",
|
||||||
@@ -234,28 +226,19 @@ func TestAzureOIDCRegression(t *testing.T) {
|
|||||||
req := httptest.NewRequest("GET", "/protected", nil)
|
req := httptest.NewRequest("GET", "/protected", nil)
|
||||||
session, _ := tOidc.sessionManager.GetSession(req)
|
session, _ := tOidc.sessionManager.GetSession(req)
|
||||||
|
|
||||||
// Set up session with opaque access token (non-JWT)
|
// Set up session with JWT access token (not opaque for this test)
|
||||||
session.SetAuthenticated(true)
|
session.SetAuthenticated(true)
|
||||||
session.SetEmail("[email protected]")
|
session.SetEmail("[email protected]")
|
||||||
session.SetAccessToken(ValidAccessToken)
|
session.SetAccessToken(ValidAccessToken) // This is actually a JWT token
|
||||||
|
|
||||||
// Create a valid ID token for claims extraction
|
// Use a valid ID token from test tokens
|
||||||
idTokenClaims := map[string]interface{}{
|
session.SetIDToken(ValidIDToken) // This token expires in 2065
|
||||||
"iss": "https://login.microsoftonline.com/tenant-id/v2.0",
|
|
||||||
"aud": "test-client-id",
|
|
||||||
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
|
||||||
"iat": time.Now().Unix(),
|
|
||||||
"sub": "user123",
|
|
||||||
"email": "[email protected]",
|
|
||||||
}
|
|
||||||
idToken, _ := createMockJWT(idTokenClaims)
|
|
||||||
session.SetIDToken(idToken)
|
|
||||||
|
|
||||||
// Mock the token verification
|
// Mock the token verification
|
||||||
originalTokenVerifier := tOidc.tokenVerifier
|
originalTokenVerifier := tOidc.tokenVerifier
|
||||||
tOidc.tokenVerifier = &mockTokenVerifier{
|
tOidc.tokenVerifier = &mockTokenVerifier{
|
||||||
verifyFunc: func(token string) error {
|
verifyFunc: func(token string) error {
|
||||||
if token == idToken {
|
if token == ValidIDToken {
|
||||||
// ID token is valid - cache claims
|
// ID token is valid - cache claims
|
||||||
testClaims := map[string]interface{}{
|
testClaims := map[string]interface{}{
|
||||||
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
|
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
|
||||||
@@ -337,16 +320,17 @@ func TestAzureOIDCRegression(t *testing.T) {
|
|||||||
|
|
||||||
// createMockJWT creates a basic JWT token for testing purposes
|
// createMockJWT creates a basic JWT token for testing purposes
|
||||||
func createMockJWT(claims map[string]interface{}) (string, error) {
|
func createMockJWT(claims map[string]interface{}) (string, error) {
|
||||||
// Simple mock JWT - in real tests you'd use a proper JWT library
|
// For testing purposes, create a JWT with expired claims when needed
|
||||||
// For this test, we'll create a basic three-part token structure
|
// Use the test tokens infrastructure for most cases, but allow expired tokens for specific tests
|
||||||
header := "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0" // {"alg":"RS256","kid":"test-key-id","typ":"JWT"}
|
testTokens := NewTestTokens()
|
||||||
|
|
||||||
// Create a simple payload with test claims
|
// Check if this is meant to be an expired token
|
||||||
payload := "eyJpc3MiOiJ0ZXN0LWlzc3VlciIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjoxNjM4MzYwMDAwLCJpYXQiOjE2MzgzNTY0MDAsInN1YiI6InVzZXIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20ifQ" // Basic claims
|
if exp, ok := claims["exp"].(int64); ok && exp < time.Now().Unix() {
|
||||||
|
return testTokens.CreateExpiredJWT(), nil
|
||||||
|
}
|
||||||
|
|
||||||
signature := "test-signature"
|
// Otherwise return a valid token
|
||||||
|
return ValidIDToken, nil
|
||||||
return header + "." + payload + "." + signature, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock error type for testing
|
// Mock error type for testing
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
@@ -222,6 +221,7 @@ type TraefikOidc struct {
|
|||||||
logger *Logger
|
logger *Logger
|
||||||
metadataRefreshStopChan chan struct{}
|
metadataRefreshStopChan chan struct{}
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
|
errorRecoveryManager *ErrorRecoveryManager
|
||||||
clientSecret string
|
clientSecret string
|
||||||
clientID string
|
clientID string
|
||||||
name string
|
name string
|
||||||
@@ -702,6 +702,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, t.logger)
|
t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, t.logger)
|
||||||
|
t.errorRecoveryManager = NewErrorRecoveryManager(t.logger)
|
||||||
t.extractClaimsFunc = extractClaims
|
t.extractClaimsFunc = extractClaims
|
||||||
// t.exchangeCodeForTokenFunc = t.exchangeCodeForToken // Removed, using interface now
|
// t.exchangeCodeForTokenFunc = t.exchangeCodeForToken // Removed, using interface now
|
||||||
t.initiateAuthenticationFunc = func(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) {
|
t.initiateAuthenticationFunc = func(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) {
|
||||||
@@ -754,8 +755,15 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
func (t *TraefikOidc) initializeMetadata(providerURL string) {
|
func (t *TraefikOidc) initializeMetadata(providerURL string) {
|
||||||
t.logger.Debug("Starting provider metadata discovery")
|
t.logger.Debug("Starting provider metadata discovery")
|
||||||
|
|
||||||
// Get metadata from cache or fetch it
|
// Get metadata from cache or fetch it with error recovery if available
|
||||||
metadata, err := t.metadataCache.GetMetadata(providerURL, t.httpClient, t.logger)
|
var metadata *ProviderMetadata
|
||||||
|
var err error
|
||||||
|
if t.errorRecoveryManager != nil {
|
||||||
|
metadata, err = t.metadataCache.GetMetadataWithRecovery(providerURL, t.httpClient, t.logger, t.errorRecoveryManager)
|
||||||
|
} else {
|
||||||
|
// Fallback for test scenarios without error recovery manager
|
||||||
|
metadata, err = t.metadataCache.GetMetadata(providerURL, t.httpClient, t.logger)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.logger.Errorf("Failed to get provider metadata: %v", err)
|
t.logger.Errorf("Failed to get provider metadata: %v", err)
|
||||||
// Consider retrying or handling this more gracefully
|
// Consider retrying or handling this more gracefully
|
||||||
@@ -812,7 +820,14 @@ func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
|||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
t.logger.Debug("Refreshing OIDC metadata")
|
t.logger.Debug("Refreshing OIDC metadata")
|
||||||
metadata, err := t.metadataCache.GetMetadata(providerURL, t.httpClient, t.logger)
|
var metadata *ProviderMetadata
|
||||||
|
var err error
|
||||||
|
if t.errorRecoveryManager != nil {
|
||||||
|
metadata, err = t.metadataCache.GetMetadataWithRecovery(providerURL, t.httpClient, t.logger, t.errorRecoveryManager)
|
||||||
|
} else {
|
||||||
|
// Fallback for test scenarios without error recovery manager
|
||||||
|
metadata, err = t.metadataCache.GetMetadata(providerURL, t.httpClient, t.logger)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.logger.Errorf("Failed to refresh metadata: %v", err)
|
t.logger.Errorf("Failed to refresh metadata: %v", err)
|
||||||
continue
|
continue
|
||||||
@@ -852,45 +867,45 @@ func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
|||||||
func discoverProviderMetadata(providerURL string, httpClient *http.Client, l *Logger) (*ProviderMetadata, error) {
|
func discoverProviderMetadata(providerURL string, httpClient *http.Client, l *Logger) (*ProviderMetadata, error) {
|
||||||
wellKnownURL := strings.TrimSuffix(providerURL, "/") + "/.well-known/openid-configuration"
|
wellKnownURL := strings.TrimSuffix(providerURL, "/") + "/.well-known/openid-configuration"
|
||||||
|
|
||||||
// Use shorter delays for tests to prevent timeouts
|
// Create retry executor with configuration optimized for test and production environments
|
||||||
maxRetries := 4 // Increased to 4 to allow for recovery after 3 failures
|
retryConfig := RetryConfig{
|
||||||
baseDelay := 10 * time.Millisecond
|
MaxAttempts: 4,
|
||||||
maxDelay := 100 * time.Millisecond
|
InitialDelay: 10 * time.Millisecond,
|
||||||
totalTimeout := 5 * time.Second
|
MaxDelay: 100 * time.Millisecond,
|
||||||
|
BackoffFactor: 2.0,
|
||||||
start := time.Now()
|
EnableJitter: true,
|
||||||
|
RetryableErrors: []string{
|
||||||
var lastErr error
|
"connection refused",
|
||||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
"timeout",
|
||||||
if time.Since(start) > totalTimeout {
|
"temporary failure",
|
||||||
l.Errorf("Timeout exceeded while fetching provider metadata")
|
"network unreachable",
|
||||||
return nil, fmt.Errorf("timeout exceeded while fetching provider metadata: %w", lastErr)
|
"no route to host",
|
||||||
}
|
"connection reset",
|
||||||
|
"status code 500",
|
||||||
metadata, err := fetchMetadata(wellKnownURL, httpClient)
|
"status code 502",
|
||||||
if err == nil {
|
"status code 503",
|
||||||
l.Debug("Provider metadata fetched successfully")
|
"status code 504",
|
||||||
return metadata, nil
|
},
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
|
|
||||||
// Don't sleep after the last attempt
|
|
||||||
if attempt < maxRetries-1 {
|
|
||||||
// Exponential backoff
|
|
||||||
delay := time.Duration(math.Pow(2, float64(attempt))) * baseDelay
|
|
||||||
if delay > maxDelay {
|
|
||||||
delay = maxDelay
|
|
||||||
}
|
|
||||||
l.Debugf("Failed to fetch provider metadata (attempt %d/%d), retrying in %s. Error: %v", attempt+1, maxRetries, delay, err)
|
|
||||||
time.Sleep(delay)
|
|
||||||
} else {
|
|
||||||
l.Debugf("Failed to fetch provider metadata (attempt %d/%d). Error: %v", attempt+1, maxRetries, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Errorf("Max retries exceeded while fetching provider metadata")
|
retryExecutor := NewRetryExecutor(retryConfig, l)
|
||||||
return nil, fmt.Errorf("max retries exceeded while fetching provider metadata: %w", lastErr)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var metadata *ProviderMetadata
|
||||||
|
err := retryExecutor.ExecuteWithContext(ctx, func() error {
|
||||||
|
var fetchErr error
|
||||||
|
metadata, fetchErr = fetchMetadata(wellKnownURL, httpClient)
|
||||||
|
return fetchErr
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
l.Errorf("Failed to fetch provider metadata after retries: %v", err)
|
||||||
|
return nil, fmt.Errorf("failed to fetch provider metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug("Provider metadata fetched successfully")
|
||||||
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchMetadata performs a single attempt to fetch and decode the OIDC provider metadata
|
// fetchMetadata performs a single attempt to fetch and decode the OIDC provider metadata
|
||||||
@@ -1986,8 +2001,19 @@ func (t *TraefikOidc) RevokeTokenWithProvider(token, tokenType string) error {
|
|||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.Header.Set("Accept", "application/json") // Prefer JSON response if available
|
req.Header.Set("Accept", "application/json") // Prefer JSON response if available
|
||||||
|
|
||||||
// Send the request
|
// Send the request with circuit breaker protection if available
|
||||||
resp, err := t.httpClient.Do(req)
|
var resp *http.Response
|
||||||
|
if t.errorRecoveryManager != nil {
|
||||||
|
serviceName := fmt.Sprintf("token-revocation-%s", t.issuerURL)
|
||||||
|
err = t.errorRecoveryManager.ExecuteWithRecovery(context.Background(), serviceName, func() error {
|
||||||
|
var reqErr error
|
||||||
|
resp, reqErr = t.httpClient.Do(req)
|
||||||
|
return reqErr
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Fallback for test scenarios without error recovery manager
|
||||||
|
resp, err = t.httpClient.Do(req)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to send token revocation request: %w", err)
|
return fmt.Errorf("failed to send token revocation request: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package traefikoidc
|
package traefikoidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -55,6 +56,90 @@ func (c *MetadataCache) isCacheValid() bool {
|
|||||||
return c.metadata != nil && time.Now().Before(c.expiresAt)
|
return c.metadata != nil && time.Now().Before(c.expiresAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMetadataWithRecovery retrieves the OIDC provider metadata with comprehensive error recovery.
|
||||||
|
// It uses circuit breaker protection and graceful degradation patterns.
|
||||||
|
// Similar to GetMetadata but with enhanced error handling capabilities.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - providerURL: The base URL of the OIDC provider.
|
||||||
|
// - httpClient: The HTTP client to use for fetching metadata.
|
||||||
|
// - logger: The logger instance for recording errors or warnings.
|
||||||
|
// - errorRecoveryManager: The error recovery manager for circuit breaker and retry handling.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A pointer to the ProviderMetadata struct.
|
||||||
|
// - An error if metadata cannot be retrieved from cache or fetched from the provider.
|
||||||
|
func (c *MetadataCache) GetMetadataWithRecovery(providerURL string, httpClient *http.Client, logger *Logger, errorRecoveryManager *ErrorRecoveryManager) (*ProviderMetadata, error) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
if c.isCacheValid() {
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
return c.metadata, nil
|
||||||
|
}
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
// Double-check after acquiring write lock
|
||||||
|
if c.isCacheValid() {
|
||||||
|
return c.metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use error recovery manager for fetching metadata with circuit breaker protection
|
||||||
|
serviceName := fmt.Sprintf("metadata-provider-%s", providerURL)
|
||||||
|
|
||||||
|
// Register fallback function for graceful degradation
|
||||||
|
errorRecoveryManager.gracefulDegradation.RegisterFallback(serviceName, func() (interface{}, error) {
|
||||||
|
if c.metadata != nil {
|
||||||
|
logger.Infof("Using cached metadata as fallback for service %s", serviceName)
|
||||||
|
// Extend cache by 10 minutes when using fallback
|
||||||
|
c.expiresAt = time.Now().Add(10 * time.Minute)
|
||||||
|
return c.metadata, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no cached metadata available for fallback")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register health check function
|
||||||
|
errorRecoveryManager.gracefulDegradation.RegisterHealthCheck(serviceName, func() bool {
|
||||||
|
// Simple health check by attempting a quick metadata fetch
|
||||||
|
_, err := discoverProviderMetadata(providerURL, httpClient, logger)
|
||||||
|
return err == nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute metadata discovery with circuit breaker and retry protection
|
||||||
|
ctx := context.Background()
|
||||||
|
var metadata *ProviderMetadata
|
||||||
|
err := errorRecoveryManager.ExecuteWithRecovery(ctx, serviceName, func() error {
|
||||||
|
var fetchErr error
|
||||||
|
metadata, fetchErr = discoverProviderMetadata(providerURL, httpClient, logger)
|
||||||
|
return fetchErr
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Try graceful degradation fallback
|
||||||
|
fallbackResult, fallbackErr := errorRecoveryManager.gracefulDegradation.ExecuteWithFallback(serviceName, func() (interface{}, error) {
|
||||||
|
return discoverProviderMetadata(providerURL, httpClient, logger)
|
||||||
|
})
|
||||||
|
|
||||||
|
if fallbackErr == nil {
|
||||||
|
if fallbackMetadata, ok := fallbackResult.(*ProviderMetadata); ok {
|
||||||
|
logger.Infof("Successfully used fallback metadata for service %s", serviceName)
|
||||||
|
c.metadata = fallbackMetadata
|
||||||
|
// Cache fallback result for 10 minutes
|
||||||
|
c.expiresAt = time.Now().Add(10 * time.Minute)
|
||||||
|
return fallbackMetadata, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to fetch provider metadata with error recovery and fallback: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.metadata = metadata
|
||||||
|
c.expiresAt = time.Now().Add(1 * time.Hour)
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetMetadata retrieves the OIDC provider metadata.
|
// GetMetadata retrieves the OIDC provider metadata.
|
||||||
// It first checks the cache for valid, non-expired metadata. If found, it's returned immediately.
|
// It first checks the cache for valid, non-expired metadata. If found, it's returned immediately.
|
||||||
// If the cache is empty or expired, it attempts to fetch the metadata from the provider's
|
// If the cache is empty or expired, it attempts to fetch the metadata from the provider's
|
||||||
|
|||||||
+685
-23
@@ -1,9 +1,12 @@
|
|||||||
package traefikoidc
|
package traefikoidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
)
|
)
|
||||||
@@ -13,6 +16,8 @@ type TokenConfig struct {
|
|||||||
Type string
|
Type string
|
||||||
MinLength int
|
MinLength int
|
||||||
MaxLength int
|
MaxLength int
|
||||||
|
MaxChunks int // Maximum number of chunks allowed
|
||||||
|
MaxChunkSize int // Maximum size per chunk
|
||||||
AllowOpaqueTokens bool
|
AllowOpaqueTokens bool
|
||||||
RequireJWTFormat bool
|
RequireJWTFormat bool
|
||||||
}
|
}
|
||||||
@@ -22,7 +27,9 @@ var (
|
|||||||
AccessTokenConfig = TokenConfig{
|
AccessTokenConfig = TokenConfig{
|
||||||
Type: "access",
|
Type: "access",
|
||||||
MinLength: 5,
|
MinLength: 5,
|
||||||
MaxLength: 50 * 1024,
|
MaxLength: 100 * 1024, // 100KB total limit
|
||||||
|
MaxChunks: 25, // Maximum 25 chunks
|
||||||
|
MaxChunkSize: maxCookieSize, // Use global chunk size limit
|
||||||
AllowOpaqueTokens: true,
|
AllowOpaqueTokens: true,
|
||||||
RequireJWTFormat: false,
|
RequireJWTFormat: false,
|
||||||
}
|
}
|
||||||
@@ -30,7 +37,9 @@ var (
|
|||||||
RefreshTokenConfig = TokenConfig{
|
RefreshTokenConfig = TokenConfig{
|
||||||
Type: "refresh",
|
Type: "refresh",
|
||||||
MinLength: 5,
|
MinLength: 5,
|
||||||
MaxLength: 50 * 1024,
|
MaxLength: 50 * 1024, // 50KB total limit (refresh tokens are typically smaller)
|
||||||
|
MaxChunks: 15, // Maximum 15 chunks
|
||||||
|
MaxChunkSize: maxCookieSize,
|
||||||
AllowOpaqueTokens: true,
|
AllowOpaqueTokens: true,
|
||||||
RequireJWTFormat: false,
|
RequireJWTFormat: false,
|
||||||
}
|
}
|
||||||
@@ -38,7 +47,9 @@ var (
|
|||||||
IDTokenConfig = TokenConfig{
|
IDTokenConfig = TokenConfig{
|
||||||
Type: "id",
|
Type: "id",
|
||||||
MinLength: 5,
|
MinLength: 5,
|
||||||
MaxLength: 50 * 1024,
|
MaxLength: 75 * 1024, // 75KB total limit
|
||||||
|
MaxChunks: 20, // Maximum 20 chunks
|
||||||
|
MaxChunkSize: maxCookieSize,
|
||||||
AllowOpaqueTokens: false,
|
AllowOpaqueTokens: false,
|
||||||
RequireJWTFormat: true,
|
RequireJWTFormat: true,
|
||||||
}
|
}
|
||||||
@@ -118,28 +129,48 @@ func (cm *ChunkManager) processSingleToken(token string, compressed bool, config
|
|||||||
|
|
||||||
// validateToken performs comprehensive token validation
|
// validateToken performs comprehensive token validation
|
||||||
func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRetrievalResult {
|
func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRetrievalResult {
|
||||||
// Length validation
|
// Enhanced size validation
|
||||||
if len(token) < config.MinLength || len(token) > config.MaxLength {
|
if sizeErr := cm.validateTokenSize(token, config); sizeErr != nil {
|
||||||
err := fmt.Errorf("CRITICAL: %s token has invalid length %d", config.Type, len(token))
|
return TokenRetrievalResult{Token: "", Error: sizeErr}
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT format validation (if required)
|
// Chunking efficiency validation (for pre-storage analysis)
|
||||||
|
if chunkErr := cm.validateChunkingEfficiency(token, config); chunkErr != nil {
|
||||||
|
return TokenRetrievalResult{Token: "", Error: chunkErr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprehensive content validation
|
||||||
|
if contentErr := cm.validateTokenContent(token, config); contentErr != nil {
|
||||||
|
return TokenRetrievalResult{Token: "", Error: contentErr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token expiration validation
|
||||||
|
if expErr := cm.validateTokenExpiration(token, config); expErr != nil {
|
||||||
|
return TokenRetrievalResult{Token: "", Error: expErr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token freshness validation
|
||||||
|
if freshnessErr := cm.validateTokenFreshness(token, config); freshnessErr != nil {
|
||||||
|
return TokenRetrievalResult{Token: "", Error: freshnessErr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced JWT format validation
|
||||||
if config.RequireJWTFormat && !config.AllowOpaqueTokens {
|
if config.RequireJWTFormat && !config.AllowOpaqueTokens {
|
||||||
dotCount := strings.Count(token, ".")
|
if validationErr := cm.validateJWTFormat(token, config.Type); validationErr != nil {
|
||||||
if dotCount != 2 {
|
return TokenRetrievalResult{Token: "", Error: validationErr}
|
||||||
err := fmt.Errorf("CRITICAL: %s token invalid JWT format (dots: %d)", config.Type, dotCount)
|
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
|
||||||
}
|
}
|
||||||
} else if config.RequireJWTFormat && config.AllowOpaqueTokens {
|
} else if config.RequireJWTFormat && config.AllowOpaqueTokens {
|
||||||
// For tokens that can be either JWT or opaque, validate JWT format only if it has dots
|
// For tokens that can be either JWT or opaque, validate JWT format only if it has dots
|
||||||
dotCount := strings.Count(token, ".")
|
dotCount := strings.Count(token, ".")
|
||||||
if dotCount > 0 && dotCount != 2 {
|
if dotCount > 0 {
|
||||||
err := fmt.Errorf("CRITICAL: %s token invalid JWT format (dots: %d)", config.Type, dotCount)
|
if validationErr := cm.validateJWTFormat(token, config.Type); validationErr != nil {
|
||||||
cm.logger.Error(err.Error())
|
return TokenRetrievalResult{Token: "", Error: validationErr}
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
}
|
||||||
|
} else {
|
||||||
|
// Validate as opaque token
|
||||||
|
if validationErr := cm.validateOpaqueToken(token, config.Type); validationErr != nil {
|
||||||
|
return TokenRetrievalResult{Token: "", Error: validationErr}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,9 +179,16 @@ func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRet
|
|||||||
|
|
||||||
// processChunkedToken handles tokens stored across multiple chunks
|
// processChunkedToken handles tokens stored across multiple chunks
|
||||||
func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, config TokenConfig) TokenRetrievalResult {
|
func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, config TokenConfig) TokenRetrievalResult {
|
||||||
// Pre-validate chunk count to prevent excessive memory usage
|
// Enhanced chunk count validation using config limits
|
||||||
if len(chunks) > 50 {
|
if len(chunks) > config.MaxChunks {
|
||||||
err := fmt.Errorf("CRITICAL: Too many %s token chunks (%d)", config.Type, len(chunks))
|
err := fmt.Errorf("CRITICAL: Too many %s token chunks (%d, max: %d)", config.Type, len(chunks), config.MaxChunks)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional safety check for extremely large chunk counts
|
||||||
|
if len(chunks) > 100 {
|
||||||
|
err := fmt.Errorf("CRITICAL: Excessive %s token chunks (%d), potential security issue", config.Type, len(chunks))
|
||||||
cm.logger.Error(err.Error())
|
cm.logger.Error(err.Error())
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
@@ -180,8 +218,18 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
|
|||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(chunk) > maxCookieSize+100 {
|
// Enhanced chunk size validation using config limits
|
||||||
err := fmt.Errorf("CRITICAL: %s token chunk %d too large", config.Type, i)
|
if len(chunk) > config.MaxChunkSize {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token chunk %d exceeds size limit (%d bytes, max: %d)",
|
||||||
|
config.Type, i, len(chunk), config.MaxChunkSize)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional safety check for extremely large chunks
|
||||||
|
if len(chunk) > maxBrowserCookieSize {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token chunk %d exceeds browser limit (%d bytes)",
|
||||||
|
config.Type, i, len(chunk))
|
||||||
cm.logger.Error(err.Error())
|
cm.logger.Error(err.Error())
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
@@ -214,3 +262,617 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
|
|||||||
|
|
||||||
return cm.validateToken(reassembledToken, config)
|
return cm.validateToken(reassembledToken, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateJWTFormat performs enhanced JWT format validation
|
||||||
|
func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error {
|
||||||
|
// Check for exactly 2 dots
|
||||||
|
dotCount := strings.Count(token, ".")
|
||||||
|
if dotCount != 2 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token invalid JWT format (dots: %d)", tokenType, dotCount)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split into parts
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token invalid JWT structure", tokenType)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each part is non-empty and contains valid base64url characters
|
||||||
|
for i, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token has empty JWT part %d", tokenType, i)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid base64url characters only (RFC 4648)
|
||||||
|
// Valid characters: A-Z, a-z, 0-9, -, _, and = for padding
|
||||||
|
for _, char := range part {
|
||||||
|
if !((char >= 'A' && char <= 'Z') ||
|
||||||
|
(char >= 'a' && char <= 'z') ||
|
||||||
|
(char >= '0' && char <= '9') ||
|
||||||
|
char == '-' || char == '_' || char == '=') {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token contains invalid base64url character in part %d", tokenType, i)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate base64url padding rules
|
||||||
|
if strings.Contains(part, "=") {
|
||||||
|
// Padding can only be at the end
|
||||||
|
paddingIndex := strings.Index(part, "=")
|
||||||
|
if paddingIndex != len(part)-1 && paddingIndex != len(part)-2 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token has invalid base64url padding in part %d", tokenType, i)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Check that after padding, no other characters exist
|
||||||
|
for j := paddingIndex; j < len(part); j++ {
|
||||||
|
if part[j] != '=' {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token has characters after padding in part %d", tokenType, i)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional length checks for JWT parts
|
||||||
|
if len(parts[0]) < 10 { // Header too short
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token header too short", tokenType)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(parts[1]) < 10 { // Payload too short
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token payload too short", tokenType)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(parts[2]) < 10 { // Signature too short
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token signature too short", tokenType)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateOpaqueToken performs validation for opaque (non-JWT) tokens
|
||||||
|
func (cm *ChunkManager) validateOpaqueToken(token string, tokenType string) error {
|
||||||
|
// Check for obviously invalid characters for opaque tokens
|
||||||
|
if strings.Contains(token, " ") {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s opaque token contains spaces", tokenType)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for control characters
|
||||||
|
for _, char := range token {
|
||||||
|
if char < 32 || char == 127 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s opaque token contains control characters", tokenType)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure minimum entropy for opaque tokens (basic check)
|
||||||
|
if len(token) >= 20 {
|
||||||
|
uniqueChars := make(map[rune]bool)
|
||||||
|
for _, char := range token {
|
||||||
|
uniqueChars[char] = true
|
||||||
|
}
|
||||||
|
// Require at least 8 unique characters for reasonable entropy
|
||||||
|
if len(uniqueChars) < 8 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s opaque token has insufficient entropy", tokenType)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTokenSize performs comprehensive token size validation
|
||||||
|
func (cm *ChunkManager) validateTokenSize(token string, config TokenConfig) error {
|
||||||
|
tokenLen := len(token)
|
||||||
|
|
||||||
|
// Basic length validation
|
||||||
|
if tokenLen < config.MinLength {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token below minimum length (%d bytes, min: %d)",
|
||||||
|
config.Type, tokenLen, config.MinLength)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenLen > config.MaxLength {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token exceeds maximum length (%d bytes, max: %d)",
|
||||||
|
config.Type, tokenLen, config.MaxLength)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT-specific size validation
|
||||||
|
if config.RequireJWTFormat || (config.AllowOpaqueTokens && strings.Contains(token, ".")) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) == 3 {
|
||||||
|
// Validate individual JWT part sizes
|
||||||
|
headerLen := len(parts[0])
|
||||||
|
payloadLen := len(parts[1])
|
||||||
|
signatureLen := len(parts[2])
|
||||||
|
|
||||||
|
// Check for unreasonably large JWT parts (potential security issue)
|
||||||
|
if headerLen > 5*1024 { // 5KB header limit
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token header too large (%d bytes)", config.Type, headerLen)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if payloadLen > config.MaxLength-10*1024 { // Leave room for header and signature
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token payload too large (%d bytes)", config.Type, payloadLen)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if signatureLen > 2*1024 { // 2KB signature limit
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token signature too large (%d bytes)", config.Type, signatureLen)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opaque token size validation
|
||||||
|
if config.AllowOpaqueTokens && !strings.Contains(token, ".") {
|
||||||
|
// For opaque tokens, check for reasonable size limits
|
||||||
|
if tokenLen > 8*1024 { // 8KB limit for opaque tokens
|
||||||
|
err := fmt.Errorf("CRITICAL: %s opaque token unusually large (%d bytes)", config.Type, tokenLen)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateChunkingEfficiency ensures that chunking is used appropriately
|
||||||
|
func (cm *ChunkManager) validateChunkingEfficiency(token string, config TokenConfig) error {
|
||||||
|
tokenLen := len(token)
|
||||||
|
|
||||||
|
// If token is small enough to fit in a single chunk, warn about unnecessary chunking
|
||||||
|
if tokenLen <= config.MaxChunkSize && tokenLen <= maxCookieSize {
|
||||||
|
// This is just informational - not an error, but helps with monitoring
|
||||||
|
cm.logger.Debugf("INFO: %s token (%d bytes) could fit in single chunk", config.Type, tokenLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate expected number of chunks
|
||||||
|
expectedChunks := (tokenLen + config.MaxChunkSize - 1) / config.MaxChunkSize
|
||||||
|
if expectedChunks > config.MaxChunks {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token would require %d chunks (max: %d)",
|
||||||
|
config.Type, expectedChunks, config.MaxChunks)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for potential storage efficiency issues
|
||||||
|
if expectedChunks > 10 && tokenLen < 50*1024 {
|
||||||
|
cm.logger.Infof("WARNING: %s token requires many chunks (%d) for size (%d bytes) - consider token optimization",
|
||||||
|
config.Type, expectedChunks, tokenLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTokenContent performs comprehensive token content validation
|
||||||
|
func (cm *ChunkManager) validateTokenContent(token string, config TokenConfig) error {
|
||||||
|
// Basic content sanitization checks
|
||||||
|
if err := cm.validateTokenSanitization(token, config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT-specific content validation
|
||||||
|
if config.RequireJWTFormat || (config.AllowOpaqueTokens && strings.Contains(token, ".")) {
|
||||||
|
if err := cm.validateJWTContent(token, config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opaque token content validation
|
||||||
|
if config.AllowOpaqueTokens && !strings.Contains(token, ".") {
|
||||||
|
if err := cm.validateOpaqueTokenContent(token, config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTokenSanitization checks for basic security issues in token content
|
||||||
|
func (cm *ChunkManager) validateTokenSanitization(token string, config TokenConfig) error {
|
||||||
|
// Check for null bytes (potential injection attacks)
|
||||||
|
if strings.Contains(token, "\x00") {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token contains null bytes", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for line feed/carriage return (header injection attacks)
|
||||||
|
if strings.ContainsAny(token, "\r\n") {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token contains line breaks", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for suspicious escape sequences
|
||||||
|
suspiciousPatterns := []string{
|
||||||
|
"\\x", "\\u", "\\n", "\\r", "\\t", "\\0",
|
||||||
|
"<script", "</script", "javascript:", "data:",
|
||||||
|
"file://", "ftp://", "ldap://",
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenLower := strings.ToLower(token)
|
||||||
|
for _, pattern := range suspiciousPatterns {
|
||||||
|
if strings.Contains(tokenLower, pattern) {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token contains suspicious pattern: %s", config.Type, pattern)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for excessive repeated characters (potential buffer overflow attempts)
|
||||||
|
if err := cm.detectRepeatedCharacters(token, config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateJWTContent performs JWT-specific content validation
|
||||||
|
func (cm *ChunkManager) validateJWTContent(token string, config TokenConfig) error {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s JWT token malformed for content validation", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate header content
|
||||||
|
if err := cm.validateJWTHeader(parts[0], config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate payload content
|
||||||
|
if err := cm.validateJWTPayload(parts[1], config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate signature content
|
||||||
|
if err := cm.validateJWTSignature(parts[2], config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateJWTHeader validates JWT header content
|
||||||
|
func (cm *ChunkManager) validateJWTHeader(header string, config TokenConfig) error {
|
||||||
|
// Basic header structure validation
|
||||||
|
if len(header) == 0 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s JWT header is empty", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate base64url encoding
|
||||||
|
if _, err := base64.RawURLEncoding.DecodeString(header); err != nil {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s JWT header not valid base64url", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateJWTPayload validates JWT payload content
|
||||||
|
func (cm *ChunkManager) validateJWTPayload(payload string, config TokenConfig) error {
|
||||||
|
// Basic payload structure validation
|
||||||
|
if len(payload) == 0 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s JWT payload is empty", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload should be decodable (basic structural check)
|
||||||
|
if _, err := base64.RawURLEncoding.DecodeString(payload); err != nil {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s JWT payload not valid base64url", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateJWTSignature validates JWT signature content
|
||||||
|
func (cm *ChunkManager) validateJWTSignature(signature string, config TokenConfig) error {
|
||||||
|
// Basic signature structure validation
|
||||||
|
if len(signature) == 0 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s JWT signature is empty", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate base64url encoding
|
||||||
|
if _, err := base64.RawURLEncoding.DecodeString(signature); err != nil {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s JWT signature not valid base64url", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateOpaqueTokenContent validates opaque token content
|
||||||
|
func (cm *ChunkManager) validateOpaqueTokenContent(token string, config TokenConfig) error {
|
||||||
|
// Check for reasonable character distribution in opaque tokens
|
||||||
|
if len(token) >= 10 {
|
||||||
|
alphabetic := 0
|
||||||
|
numeric := 0
|
||||||
|
special := 0
|
||||||
|
|
||||||
|
for _, char := range token {
|
||||||
|
if (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') {
|
||||||
|
alphabetic++
|
||||||
|
} else if char >= '0' && char <= '9' {
|
||||||
|
numeric++
|
||||||
|
} else {
|
||||||
|
special++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total := alphabetic + numeric + special
|
||||||
|
if total > 0 {
|
||||||
|
// Require some distribution of character types for legitimate tokens
|
||||||
|
alphaRatio := float64(alphabetic) / float64(total)
|
||||||
|
numericRatio := float64(numeric) / float64(total)
|
||||||
|
|
||||||
|
// Opaque tokens should have reasonable character distribution
|
||||||
|
if alphaRatio < 0.1 && numericRatio < 0.1 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s opaque token has suspicious character distribution", config.Type)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common token prefixes/suffixes that might indicate legitimate tokens
|
||||||
|
legitimatePrefixes := []string{
|
||||||
|
"Bearer ", "bearer ", "eyJ", // JWT prefix
|
||||||
|
"refresh_", "access_", "id_",
|
||||||
|
"token_", "oauth_", "oidc_",
|
||||||
|
}
|
||||||
|
|
||||||
|
hasLegitimatePrefix := false
|
||||||
|
for _, prefix := range legitimatePrefixes {
|
||||||
|
if strings.HasPrefix(token, prefix) {
|
||||||
|
hasLegitimatePrefix = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For longer tokens without legitimate prefixes, be more suspicious
|
||||||
|
if len(token) > 50 && !hasLegitimatePrefix {
|
||||||
|
cm.logger.Debugf("WARNING: %s opaque token lacks common token prefixes", config.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectRepeatedCharacters detects potential buffer overflow attempts
|
||||||
|
func (cm *ChunkManager) detectRepeatedCharacters(token string, config TokenConfig) error {
|
||||||
|
if len(token) < 10 {
|
||||||
|
return nil // Too short to analyze meaningfully
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count consecutive repeated characters
|
||||||
|
maxRepeated := 0
|
||||||
|
currentRepeated := 1
|
||||||
|
var lastChar rune
|
||||||
|
|
||||||
|
for i, char := range token {
|
||||||
|
if i > 0 && char == lastChar {
|
||||||
|
currentRepeated++
|
||||||
|
if currentRepeated > maxRepeated {
|
||||||
|
maxRepeated = currentRepeated
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentRepeated = 1
|
||||||
|
}
|
||||||
|
lastChar = char
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag tokens with excessive character repetition
|
||||||
|
threshold := 20 // Allow up to 20 consecutive identical characters
|
||||||
|
if maxRepeated > threshold {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token has excessive repeated characters (%d consecutive)",
|
||||||
|
config.Type, maxRepeated)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for overall character frequency (detect padding attacks)
|
||||||
|
charFreq := make(map[rune]int)
|
||||||
|
for _, char := range token {
|
||||||
|
charFreq[char]++
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenLen := len(token)
|
||||||
|
for char, count := range charFreq {
|
||||||
|
frequency := float64(count) / float64(tokenLen)
|
||||||
|
|
||||||
|
// Flag if any single character makes up more than 70% of the token
|
||||||
|
if frequency > 0.7 && tokenLen > 20 {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token has suspicious character frequency (char '%c': %.1f%%)",
|
||||||
|
config.Type, char, frequency*100)
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTokenExpiration validates token expiration during storage/retrieval
|
||||||
|
func (cm *ChunkManager) validateTokenExpiration(token string, config TokenConfig) error {
|
||||||
|
// Only validate expiration for JWT tokens
|
||||||
|
if !strings.Contains(token, ".") {
|
||||||
|
return nil // Opaque tokens don't have embedded expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JWT expiration claim
|
||||||
|
expiration, err := cm.extractJWTExpiration(token)
|
||||||
|
if err != nil {
|
||||||
|
// If we can't parse expiration, log it but don't fail - the token might be valid but malformed
|
||||||
|
cm.logger.Debugf("Could not extract expiration from %s token: %v", config.Type, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is expired
|
||||||
|
if expiration != nil && time.Now().After(*expiration) {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token is expired (expired at: %v)", config.Type, expiration.Format(time.RFC3339))
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token expires too far in the future (potential security issue)
|
||||||
|
if expiration != nil {
|
||||||
|
maxFutureTime := time.Now().Add(10 * 365 * 24 * time.Hour) // 10 years
|
||||||
|
if expiration.After(maxFutureTime) {
|
||||||
|
cm.logger.Infof("WARNING: %s token expires very far in future (%v) - potential security issue",
|
||||||
|
config.Type, expiration.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractJWTExpiration extracts the expiration time from a JWT token
|
||||||
|
func (cm *ChunkManager) extractJWTExpiration(token string) (*time.Time, error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid JWT format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the payload (second part)
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode JWT payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON payload
|
||||||
|
var claims map[string]interface{}
|
||||||
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse JWT claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract expiration claim
|
||||||
|
exp, exists := claims["exp"]
|
||||||
|
if !exists {
|
||||||
|
return nil, nil // No expiration claim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert expiration to time.Time
|
||||||
|
var expTime time.Time
|
||||||
|
switch v := exp.(type) {
|
||||||
|
case float64:
|
||||||
|
expTime = time.Unix(int64(v), 0)
|
||||||
|
case int64:
|
||||||
|
expTime = time.Unix(v, 0)
|
||||||
|
case int:
|
||||||
|
expTime = time.Unix(int64(v), 0)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid expiration format: %T", exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &expTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTokenFreshness checks if token is fresh enough for storage
|
||||||
|
func (cm *ChunkManager) validateTokenFreshness(token string, config TokenConfig) error {
|
||||||
|
// Only validate freshness for JWT tokens
|
||||||
|
if !strings.Contains(token, ".") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract issued at time
|
||||||
|
issuedAt, err := cm.extractJWTIssuedAt(token)
|
||||||
|
if err != nil {
|
||||||
|
cm.logger.Debugf("Could not extract issued time from %s token: %v", config.Type, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if issuedAt != nil {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Check if token was issued in the future (clock skew tolerance: 5 minutes)
|
||||||
|
if issuedAt.After(now.Add(5 * time.Minute)) {
|
||||||
|
err := fmt.Errorf("CRITICAL: %s token issued in future (issued at: %v)",
|
||||||
|
config.Type, issuedAt.Format(time.RFC3339))
|
||||||
|
cm.logger.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is too old (potential replay attack)
|
||||||
|
maxAge := 24 * time.Hour // Tokens older than 24 hours are suspicious
|
||||||
|
if now.Sub(*issuedAt) > maxAge {
|
||||||
|
cm.logger.Infof("WARNING: %s token is quite old (issued: %v) - potential replay",
|
||||||
|
config.Type, issuedAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractJWTIssuedAt extracts the issued at time from a JWT token
|
||||||
|
func (cm *ChunkManager) extractJWTIssuedAt(token string) (*time.Time, error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid JWT format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the payload (second part)
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode JWT payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON payload
|
||||||
|
var claims map[string]interface{}
|
||||||
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse JWT claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract issued at claim
|
||||||
|
iat, exists := claims["iat"]
|
||||||
|
if !exists {
|
||||||
|
return nil, nil // No issued at claim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert issued at to time.Time
|
||||||
|
var iatTime time.Time
|
||||||
|
switch v := iat.(type) {
|
||||||
|
case float64:
|
||||||
|
iatTime = time.Unix(int64(v), 0)
|
||||||
|
case int64:
|
||||||
|
iatTime = time.Unix(v, 0)
|
||||||
|
case int:
|
||||||
|
iatTime = time.Unix(int64(v), 0)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid issued at format: %T", iat)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &iatTime, nil
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -549,7 +549,7 @@ func TestTokenSizeLimits(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Large but acceptable token",
|
name: "Large but acceptable token",
|
||||||
tokenSize: 30000, // FIXED: 30KB to ensure final size < 100KB limit
|
tokenSize: 20000, // 20KB to ensure it fits within chunk limits (≤25 chunks)
|
||||||
expectStored: true,
|
expectStored: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+20
-8
@@ -20,16 +20,16 @@ func NewTestTokens() *TestTokens {
|
|||||||
// Valid JWT tokens for testing
|
// Valid JWT tokens for testing
|
||||||
const (
|
const (
|
||||||
// ValidAccessToken - A properly formatted JWT access token for testing
|
// ValidAccessToken - A properly formatted JWT access token for testing
|
||||||
ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOiJ0ZXN0LWNsaWVudC1pZCIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MTc1MDI5NDYyOCwiaWF0IjoxNzUwMjkxMDI4LCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJqdGkiOiJlNDcxN2RhZDBmZjAyOTNkIiwibmJmIjoxNzUwMjkxMDI4LCJub25jZSI6Im5vbmNlMTIzIiwic3ViIjoidGVzdC1zdWJqZWN0In0.bmwp-vk0B7Ir9UiUkzib8L7yJbebJ00o3U9QrB6gP2H9-RfqyCbN8M9Rkx7Rb8Vdh3YzqkBBoLS_G0i414rs2I9uABnTC4E6-63qkGdUrLB7p-XbjcRW2RoIBwXHk7lfumi8eX0uWzBsJ9CY0__UECVsex5XORfBb4Bcqj0LK4y-glxkpI51I7BPySfciWC_PkdaQ1Qe5pCAlxeNs2E9NMGXp-Ox6vAufUzoC2cws1LswGPPP6icQ-Zlzd5WMCIWhdIkN4yTxk8FMqsTC52k2zskRHNSSd4DDVETonfzawZNqDcMpnTyN53sCJ9UHiQTl9mCm61ttYW-W9Gc-ze4Xw"
|
ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOiJ0ZXN0LWNsaWVudC1pZCIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MzAwMDAwMDAwMCwiaWF0IjoxMDAwMDAwMDAwLCJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImp0aSI6ImU0NzE3ZGFkMGZmMDI5M2QiLCJuYmYiOjEwMDAwMDAwMDAsIm5vbmNlIjoibm9uY2UxMjMiLCJzdWIiOiJ0ZXN0LXN1YmplY3QifQ.bmwp-vk0B7Ir9UiUkzib8L7yJbebJ00o3U9QrB6gP2H9-RfqyCbN8M9Rkx7Rb8Vdh3YzqkBBoLS_G0i414rs2I9uABnTC4E6-63qkGdUrLB7p-XbjcRW2RoIBwXHk7lfumi8eX0uWzBsJ9CY0__UECVsex5XORfBb4Bcqj0LK4y-glxkpI51I7BPySfciWC_PkdaQ1Qe5pCAlxeNs2E9NMGXp-Ox6vAufUzoC2cws1LswGPPP6icQ-Zlzd5WMCIWhdIkN4yTxk8FMqsTC52k2zskRHNSSd4DDVETonfzawZNqDcMpnTyN53sCJ9UHiQTl9mCm61ttYW-W9Gc-ze4Xw"
|
||||||
|
|
||||||
// ValidIDToken - A properly formatted JWT ID token for testing
|
// ValidIDToken - A properly formatted JWT ID token for testing
|
||||||
ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOiJ0ZXN0LWNsaWVudC1pZCIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MTc1MDI5NDYyOCwiaWF0IjoxNzUwMjkxMDI4LCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJqdGkiOiI2YzBjZTZmMTM4Y2EzMzc2IiwibmJmIjoxNzUwMjkxMDI4LCJub25jZSI6Im5vbmNlMTIzIiwic3ViIjoidGVzdC1zdWJqZWN0In0.RBQYejA9vP4lnh2EhFqWerePWaCyDTF0ZE1jlU2xm4g2wWVeaEHpv5SNg92_gwk633N9xx7ugS0UrlEu4qbT7wSb1HBDR00q_andyYnyFk4OoxPpD0AqHkVr-pjS-Z7UCGF3sLgQ4ECmU9695PIys3XvgUGMzEn_mK-PHcpY5AnbBGFsbj7epUld_sb6WfjjjwAa8kKfKObPvaIpuJ4TlxI1Uf0wYOoIA0zh5ipeAn-i8Ud-GErxis1Hp8UQK7IRolXpToiXnFcnf3vI3eCS7Yu3oPl7LRxTxKMCI9h0MCwu25ZNsOg2C9ohyebpU0jbURX9Q74GNOaphv-Lz9rCRA"
|
ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOiJ0ZXN0LWNsaWVudC1pZCIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MzAwMDAwMDAwMCwiaWF0IjoxMDAwMDAwMDAwLCJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImp0aSI6IjZjMGNlNmYxMzhjYTMzNzYiLCJuYmYiOjEwMDAwMDAwMDAsIm5vbmNlIjoibm9uY2UxMjMiLCJzdWIiOiJ0ZXN0LXN1YmplY3QifQ.RBQYejA9vP4lnh2EhFqWerePWaCyDTF0ZE1jlU2xm4g2wWVeaEHpv5SNg92_gwk633N9xx7ugS0UrlEu4qbT7wSb1HBDR00q_andyYnyFk4OoxPpD0AqHkVr-pjS-Z7UCGF3sLgQ4ECmU9695PIys3XvgUGMzEn_mK-PHcpY5AnbBGFsbj7epUld_sb6WfjjjwAa8kKfKObPvaIpuJ4TlxI1Uf0wYOoIA0zh5ipeAn-i8Ud-GErxis1Hp8UQK7IRolXpToiXnFcnf3vI3eCS7Yu3oPl7LRxTxKMCI9h0MCwu25ZNsOg2C9ohyebpU0jbURX9Q74GNOaphv-Lz9rCRA"
|
||||||
|
|
||||||
// ValidRefreshToken - A properly formatted refresh token for testing
|
// ValidRefreshToken - A properly formatted refresh token for testing
|
||||||
ValidRefreshToken = "valid-refresh-token-12345"
|
ValidRefreshToken = "valid-refresh-token-12345"
|
||||||
|
|
||||||
// MinimalValidJWT - The shortest valid JWT for testing
|
// MinimalValidJWT - The shortest valid JWT for testing (actual base64url)
|
||||||
MinimalValidJWT = "h.p.s"
|
MinimalValidJWT = "eyJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxMjMifQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"
|
||||||
|
|
||||||
// ValidRefreshTokenGoogle - A Google-style refresh token for testing
|
// ValidRefreshTokenGoogle - A Google-style refresh token for testing
|
||||||
ValidRefreshTokenGoogle = "google_refresh_token_12345"
|
ValidRefreshTokenGoogle = "google_refresh_token_12345"
|
||||||
@@ -57,7 +57,10 @@ const (
|
|||||||
// This replaces the ad-hoc createLargeValidJWT function in tests
|
// This replaces the ad-hoc createLargeValidJWT function in tests
|
||||||
func (tt *TestTokens) CreateLargeValidJWT(targetSize int) string {
|
func (tt *TestTokens) CreateLargeValidJWT(targetSize int) string {
|
||||||
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||||
signature := "signature_" + tt.generateRandomString(32)
|
// Create a valid base64url signature
|
||||||
|
signatureBytes := make([]byte, 32)
|
||||||
|
rand.Read(signatureBytes)
|
||||||
|
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||||
|
|
||||||
// Calculate required payload size
|
// Calculate required payload size
|
||||||
usedSize := len(header) + len(signature) + 2 // account for dots
|
usedSize := len(header) + len(signature) + 2 // account for dots
|
||||||
@@ -110,7 +113,10 @@ func (tt *TestTokens) CreateExpiredJWT() string {
|
|||||||
|
|
||||||
claimsJSON, _ := json.Marshal(claims)
|
claimsJSON, _ := json.Marshal(claims)
|
||||||
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||||
signature := "expired_signature"
|
// Create a valid base64url signature
|
||||||
|
signatureBytes := make([]byte, 16)
|
||||||
|
rand.Read(signatureBytes)
|
||||||
|
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||||
|
|
||||||
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
||||||
}
|
}
|
||||||
@@ -130,7 +136,10 @@ func (tt *TestTokens) CreateUniqueValidJWT(id string) string {
|
|||||||
|
|
||||||
claimsJSON, _ := json.Marshal(claims)
|
claimsJSON, _ := json.Marshal(claims)
|
||||||
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||||
signature := "sig_" + id
|
// Create a valid base64url signature
|
||||||
|
signatureBytes := make([]byte, 16)
|
||||||
|
rand.Read(signatureBytes)
|
||||||
|
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||||
|
|
||||||
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
||||||
}
|
}
|
||||||
@@ -139,7 +148,10 @@ func (tt *TestTokens) CreateUniqueValidJWT(id string) string {
|
|||||||
// This is useful for testing chunking scenarios where compression doesn't help
|
// This is useful for testing chunking scenarios where compression doesn't help
|
||||||
func (tt *TestTokens) CreateIncompressibleToken(targetSize int) string {
|
func (tt *TestTokens) CreateIncompressibleToken(targetSize int) string {
|
||||||
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||||
signature := "incompressible_signature_" + tt.generateRandomString(32)
|
// Create a valid base64url signature
|
||||||
|
signatureBytes := make([]byte, 32)
|
||||||
|
rand.Read(signatureBytes)
|
||||||
|
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||||
|
|
||||||
// Calculate required payload size
|
// Calculate required payload size
|
||||||
usedSize := len(header) + len(signature) + 2 // account for dots
|
usedSize := len(header) + len(signature) + 2 // account for dots
|
||||||
|
|||||||
+15
-45
@@ -3,7 +3,6 @@ package traefikoidc
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -23,8 +22,9 @@ func TestTokenCorruptionScenario(t *testing.T) {
|
|||||||
t.Fatalf("Failed to create session manager: %v", err)
|
t.Fatalf("Failed to create session manager: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a valid JWT token
|
// Create a valid JWT token with proper base64url signature
|
||||||
validJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6OTk5OTk5OTk5OX0.signature"
|
testTokens := NewTestTokens()
|
||||||
|
validJWT := testTokens.CreateLargeValidJWT(100) // Create a small valid token
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
corruptionScenario func(*SessionData)
|
corruptionScenario func(*SessionData)
|
||||||
@@ -146,7 +146,7 @@ func TestCompressionIntegrityFailure(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid JWT",
|
name: "Valid JWT",
|
||||||
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig",
|
token: NewTestTokens().CreateLargeValidJWT(100),
|
||||||
expectSame: true,
|
expectSame: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -193,7 +193,8 @@ func TestChunkReassemblyEdgeCases(t *testing.T) {
|
|||||||
defer session.ReturnToPool()
|
defer session.ReturnToPool()
|
||||||
|
|
||||||
// Create a large token that will definitely be chunked
|
// Create a large token that will definitely be chunked
|
||||||
largeToken := createTokenOfSize("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig", 8000)
|
testTokens := NewTestTokens()
|
||||||
|
largeToken := testTokens.CreateLargeValidJWT(8000)
|
||||||
|
|
||||||
// Store the token to create chunks
|
// Store the token to create chunks
|
||||||
session.SetAccessToken(largeToken)
|
session.SetAccessToken(largeToken)
|
||||||
@@ -311,10 +312,11 @@ func TestRaceConditionProtection(t *testing.T) {
|
|||||||
const numOperations = 50
|
const numOperations = 50
|
||||||
|
|
||||||
// Create tokens of different sizes
|
// Create tokens of different sizes
|
||||||
|
testTokens := NewTestTokens()
|
||||||
tokens := []string{
|
tokens := []string{
|
||||||
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig1",
|
testTokens.CreateUniqueValidJWT("token1"),
|
||||||
createTokenOfSize("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig2", 3000),
|
testTokens.CreateLargeValidJWT(3000),
|
||||||
createTokenOfSize("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig3", 6000),
|
testTokens.CreateLargeValidJWT(6000),
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -443,7 +445,8 @@ func TestBackwardCompatibility(t *testing.T) {
|
|||||||
defer session.ReturnToPool()
|
defer session.ReturnToPool()
|
||||||
|
|
||||||
// Simulate old-style session data (without new validation fields)
|
// Simulate old-style session data (without new validation fields)
|
||||||
oldStyleToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.oldsig"
|
testTokens := NewTestTokens()
|
||||||
|
oldStyleToken := testTokens.CreateUniqueValidJWT("old")
|
||||||
|
|
||||||
// Manually set token without going through new SetAccessToken validation
|
// Manually set token without going through new SetAccessToken validation
|
||||||
session.accessSession.Values["token"] = oldStyleToken
|
session.accessSession.Values["token"] = oldStyleToken
|
||||||
@@ -467,41 +470,8 @@ func TestBackwardCompatibility(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createTokenOfSize creates a JWT token of approximately the specified size
|
// createTokenOfSize creates a JWT token of approximately the specified size
|
||||||
|
// This function is deprecated - use TestTokens.CreateLargeValidJWT instead
|
||||||
func createTokenOfSize(baseToken string, targetSize int) string {
|
func createTokenOfSize(baseToken string, targetSize int) string {
|
||||||
parts := strings.Split(baseToken, ".")
|
testTokens := NewTestTokens()
|
||||||
if len(parts) != 3 {
|
return testTokens.CreateLargeValidJWT(targetSize)
|
||||||
return baseToken
|
|
||||||
}
|
|
||||||
|
|
||||||
header, payload, signature := parts[0], parts[1], parts[2]
|
|
||||||
currentSize := len(baseToken)
|
|
||||||
|
|
||||||
if currentSize >= targetSize {
|
|
||||||
return baseToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand the payload to reach target size
|
|
||||||
paddingNeeded := targetSize - len(header) - len(signature) - 2 // Account for dots
|
|
||||||
if paddingNeeded > 0 {
|
|
||||||
// Decode current payload, add padding, re-encode
|
|
||||||
decoded, err := base64.RawURLEncoding.DecodeString(payload)
|
|
||||||
if err != nil {
|
|
||||||
// If we can't decode, just pad with random base64-safe characters to resist compression
|
|
||||||
randomBytes := make([]byte, paddingNeeded)
|
|
||||||
rand.Read(randomBytes)
|
|
||||||
// Encode as base64 to make it base64-safe
|
|
||||||
padData := base64.RawURLEncoding.EncodeToString(randomBytes)
|
|
||||||
payload = payload + padData
|
|
||||||
} else {
|
|
||||||
// Add padding to the JSON - use random data to resist compression
|
|
||||||
randomBytes := make([]byte, paddingNeeded/2)
|
|
||||||
rand.Read(randomBytes)
|
|
||||||
// Encode as base64 to make it JSON-safe
|
|
||||||
padData := base64.StdEncoding.EncodeToString(randomBytes)
|
|
||||||
newPayload := fmt.Sprintf(`{"original":%s,"padding":"%s"}`, string(decoded), padData)
|
|
||||||
payload = base64.RawURLEncoding.EncodeToString([]byte(newPayload))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package traefikoidc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -332,9 +333,9 @@ func TestTokenCorruptionIntegrationFlows(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Normal flow - small tokens",
|
name: "Normal flow - small tokens",
|
||||||
accessToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.access_sig",
|
accessToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.access_signature_data_here",
|
||||||
refreshToken: "refresh_token_12345",
|
refreshToken: "refresh_token_12345",
|
||||||
idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.id_sig",
|
idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.id_token_signature_data_here",
|
||||||
expectSuccess: true,
|
expectSuccess: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -348,7 +349,7 @@ func TestTokenCorruptionIntegrationFlows(t *testing.T) {
|
|||||||
name: "Corrupted access token compression",
|
name: "Corrupted access token compression",
|
||||||
accessToken: createLargeValidJWT(3000),
|
accessToken: createLargeValidJWT(3000),
|
||||||
refreshToken: "refresh_token_12345",
|
refreshToken: "refresh_token_12345",
|
||||||
idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.id_sig",
|
idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.id_token_signature_data_here",
|
||||||
expectSuccess: false,
|
expectSuccess: false,
|
||||||
corruptAction: func(session *SessionData) {
|
corruptAction: func(session *SessionData) {
|
||||||
// Corrupt compressed access token
|
// Corrupt compressed access token
|
||||||
@@ -362,7 +363,7 @@ func TestTokenCorruptionIntegrationFlows(t *testing.T) {
|
|||||||
name: "Corrupted chunk in large token",
|
name: "Corrupted chunk in large token",
|
||||||
accessToken: createLargeValidJWT(15000), // Force chunking with larger size
|
accessToken: createLargeValidJWT(15000), // Force chunking with larger size
|
||||||
refreshToken: "refresh_token_12345",
|
refreshToken: "refresh_token_12345",
|
||||||
idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.id_sig",
|
idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.id_token_signature_data_here",
|
||||||
expectSuccess: false,
|
expectSuccess: false,
|
||||||
corruptAction: func(session *SessionData) {
|
corruptAction: func(session *SessionData) {
|
||||||
// Corrupt first chunk if chunked, otherwise corrupt single token
|
// Corrupt first chunk if chunked, otherwise corrupt single token
|
||||||
@@ -647,7 +648,10 @@ func TestTokenValidationEdgeCases(t *testing.T) {
|
|||||||
// createLargeValidJWT creates a JWT of approximately the specified size
|
// createLargeValidJWT creates a JWT of approximately the specified size
|
||||||
func createLargeValidJWT(targetSize int) string {
|
func createLargeValidJWT(targetSize int) string {
|
||||||
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||||
signature := "signature_" + generateRandomString(32)
|
// Create a valid base64url signature
|
||||||
|
signatureBytes := make([]byte, 32)
|
||||||
|
rand.Read(signatureBytes)
|
||||||
|
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||||
|
|
||||||
// Calculate required payload size
|
// Calculate required payload size
|
||||||
usedSize := len(header) + len(signature) + 2 // account for dots
|
usedSize := len(header) + len(signature) + 2 // account for dots
|
||||||
|
|||||||
Reference in New Issue
Block a user