mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
9cbca4c4fb
patch-release
The refresh path in token_manager.go hardcoded the "email" claim when
extracting the user identifier from a refreshed ID token, ignoring the
configured userIdentifierClaim. Keycloak users without an email claim
(using sub or another identifier) were kicked out on refresh even
though their initial login worked.
The callback path (auth_flow.go:226-239) already honored
userIdentifierClaim with "sub" fallback; PR #100 (commit a316a98)
added that support but missed the refresh path.
Mirror the callback logic in refreshToken so both paths behave the same.
Cleanup: rename Get/SetEmail to Get/SetUserIdentifier on SessionData
to match the actual semantics. The slot already stored the configured
identifier (email, sub, oid, upn, preferred_username), only the API
name was misleading. Storage key "email" → "user_identifier" and
combinedSessionPayload field E (json:"e") → Ui (json:"ui").
Compat note: existing user sessions invalidate on upgrade — every active
user re-authenticates once after deploying this change.
794 lines
25 KiB
Go
794 lines
25 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
// SessionBehaviourSuite tests session management behavior
|
|
type SessionBehaviourSuite struct {
|
|
suite.Suite
|
|
logger *Logger
|
|
sessionManager *SessionManager
|
|
}
|
|
|
|
func (s *SessionBehaviourSuite) SetupTest() {
|
|
s.logger = NewLogger("error")
|
|
|
|
var err error
|
|
s.sessionManager, err = NewSessionManager(
|
|
"test-encryption-key-32-bytes-long!!",
|
|
false,
|
|
"",
|
|
"",
|
|
0,
|
|
s.logger,
|
|
)
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
func (s *SessionBehaviourSuite) TearDownTest() {
|
|
if s.sessionManager != nil {
|
|
s.sessionManager.Shutdown()
|
|
}
|
|
}
|
|
|
|
// TestValidateSessionHealth_NilSession tests validation with nil session
|
|
func (s *SessionBehaviourSuite) TestValidateSessionHealth_NilSession() {
|
|
err := s.sessionManager.ValidateSessionHealth(nil)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "session data is nil")
|
|
}
|
|
|
|
// TestValidateSessionHealth_NotAuthenticated tests validation with unauthenticated session
|
|
func (s *SessionBehaviourSuite) TestValidateSessionHealth_NotAuthenticated() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Session is not authenticated by default
|
|
err = s.sessionManager.ValidateSessionHealth(session)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "session is not authenticated")
|
|
}
|
|
|
|
// TestValidateSessionHealth_AuthenticatedSession tests validation with authenticated session
|
|
func (s *SessionBehaviourSuite) TestValidateSessionHealth_AuthenticatedSession() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set session as authenticated
|
|
err = session.SetAuthenticated(true)
|
|
s.Require().NoError(err)
|
|
|
|
// Validate health - should pass
|
|
err = s.sessionManager.ValidateSessionHealth(session)
|
|
s.NoError(err)
|
|
}
|
|
|
|
// TestValidateSessionHealth_WithValidAccessToken tests validation with valid access token
|
|
func (s *SessionBehaviourSuite) TestValidateSessionHealth_WithValidAccessToken() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set session as authenticated
|
|
err = session.SetAuthenticated(true)
|
|
s.Require().NoError(err)
|
|
|
|
// Set a valid-format access token (opaque token format)
|
|
session.SetAccessToken("valid-access-token-with-sufficient-length-for-testing")
|
|
|
|
// Validate health - should pass
|
|
err = s.sessionManager.ValidateSessionHealth(session)
|
|
s.NoError(err)
|
|
}
|
|
|
|
// TestValidateSessionHealth_CorruptedAccessToken tests validation with corrupted access token
|
|
func (s *SessionBehaviourSuite) TestValidateSessionHealth_CorruptedAccessToken() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set session as authenticated
|
|
err = session.SetAuthenticated(true)
|
|
s.Require().NoError(err)
|
|
|
|
// Manually set a corrupted access token
|
|
session.accessSession.Values["token"] = "__CORRUPTION_MARKER_TEST__"
|
|
session.accessSession.Values["compressed"] = false
|
|
|
|
// Validate health - should fail
|
|
err = s.sessionManager.ValidateSessionHealth(session)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "access token validation failed")
|
|
}
|
|
|
|
// TestValidateSessionHealth_PathTraversalAttempt tests detection of path traversal in session
|
|
func (s *SessionBehaviourSuite) TestValidateSessionHealth_PathTraversalAttempt() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set session as authenticated
|
|
err = session.SetAuthenticated(true)
|
|
s.Require().NoError(err)
|
|
|
|
// Inject path traversal attempt in session value
|
|
session.mainSession.Values["malicious"] = "../../../etc/passwd"
|
|
|
|
// Validate health - should detect tampering
|
|
err = s.sessionManager.ValidateSessionHealth(session)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "tampering detected")
|
|
}
|
|
|
|
// TestValidateSessionHealth_XSSAttempt tests detection of XSS attempt in session
|
|
func (s *SessionBehaviourSuite) TestValidateSessionHealth_XSSAttempt() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set session as authenticated
|
|
err = session.SetAuthenticated(true)
|
|
s.Require().NoError(err)
|
|
|
|
// Inject XSS attempt in session value
|
|
session.mainSession.Values["xss"] = "<script>alert('xss')</script>"
|
|
|
|
// Validate health - should detect tampering
|
|
err = s.sessionManager.ValidateSessionHealth(session)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "tampering detected")
|
|
}
|
|
|
|
// TestValidateSessionHealth_SuspiciouslyLongValue tests detection of suspiciously long values
|
|
func (s *SessionBehaviourSuite) TestValidateSessionHealth_SuspiciouslyLongValue() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set session as authenticated
|
|
err = session.SetAuthenticated(true)
|
|
s.Require().NoError(err)
|
|
|
|
// Inject suspiciously long value
|
|
session.mainSession.Values["long_value"] = strings.Repeat("x", 15000)
|
|
|
|
// Validate health - should detect suspicious value
|
|
err = s.sessionManager.ValidateSessionHealth(session)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "suspiciously long")
|
|
}
|
|
|
|
// TestValidateTokenFormat_EmptyToken tests validation of empty token
|
|
func (s *SessionBehaviourSuite) TestValidateTokenFormat_EmptyToken() {
|
|
err := s.sessionManager.validateTokenFormat("", "access_token")
|
|
s.NoError(err) // Empty tokens are valid (just not present)
|
|
}
|
|
|
|
// TestValidateTokenFormat_ValidJWT tests validation of valid JWT format
|
|
func (s *SessionBehaviourSuite) TestValidateTokenFormat_ValidJWT() {
|
|
// Valid JWT format (header.payload.signature)
|
|
jwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature"
|
|
err := s.sessionManager.validateTokenFormat(jwt, "id_token")
|
|
s.NoError(err)
|
|
}
|
|
|
|
// TestValidateTokenFormat_InvalidJWTWithEmptyPart tests JWT with empty part
|
|
func (s *SessionBehaviourSuite) TestValidateTokenFormat_InvalidJWTWithEmptyPart() {
|
|
// JWT with empty part
|
|
invalidJWT := "header..signature"
|
|
err := s.sessionManager.validateTokenFormat(invalidJWT, "id_token")
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "empty part")
|
|
}
|
|
|
|
// TestValidateTokenFormat_CorruptionMarker tests detection of corruption marker
|
|
func (s *SessionBehaviourSuite) TestValidateTokenFormat_CorruptionMarker() {
|
|
err := s.sessionManager.validateTokenFormat("__CORRUPTION_MARKER_TEST__", "access_token")
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "corruption marker")
|
|
}
|
|
|
|
// TestPeriodicChunkCleanup tests the periodic cleanup function
|
|
func (s *SessionBehaviourSuite) TestPeriodicChunkCleanup() {
|
|
// This should not panic or error
|
|
s.sessionManager.PeriodicChunkCleanup()
|
|
|
|
// Verify it can be called multiple times
|
|
s.sessionManager.PeriodicChunkCleanup()
|
|
s.sessionManager.PeriodicChunkCleanup()
|
|
}
|
|
|
|
// TestPeriodicChunkCleanup_WithCanceledContext tests cleanup with canceled context
|
|
func (s *SessionBehaviourSuite) TestPeriodicChunkCleanup_WithCanceledContext() {
|
|
// Cancel the context
|
|
s.sessionManager.cancel()
|
|
|
|
// Should return early without panicking
|
|
s.sessionManager.PeriodicChunkCleanup()
|
|
}
|
|
|
|
// TestGetSessionStats tests session statistics retrieval
|
|
func (s *SessionBehaviourSuite) TestGetSessionStats() {
|
|
stats := s.sessionManager.GetSessionStats()
|
|
|
|
s.NotNil(stats)
|
|
s.Contains(stats, "active_sessions")
|
|
s.Contains(stats, "pool_hits")
|
|
s.Contains(stats, "pool_misses")
|
|
}
|
|
|
|
// TestGetSessionMetrics tests session metrics retrieval
|
|
func (s *SessionBehaviourSuite) TestGetSessionMetrics() {
|
|
metrics := s.sessionManager.GetSessionMetrics()
|
|
|
|
s.NotNil(metrics)
|
|
s.Equal("CookieStore", metrics["session_manager_type"])
|
|
s.Contains(metrics, "force_https")
|
|
s.Contains(metrics, "absolute_timeout_hours")
|
|
s.Contains(metrics, "max_cookie_size")
|
|
s.Contains(metrics, "has_encryption")
|
|
}
|
|
|
|
// TestEnhanceSessionSecurity_NilOptions tests enhancing nil options
|
|
func (s *SessionBehaviourSuite) TestEnhanceSessionSecurity_NilOptions() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
options := s.sessionManager.EnhanceSessionSecurity(nil, req)
|
|
|
|
s.NotNil(options)
|
|
s.True(options.HttpOnly)
|
|
s.Equal("/", options.Path)
|
|
}
|
|
|
|
// TestEnhanceSessionSecurity_WithHTTPS tests enhancing with HTTPS request
|
|
func (s *SessionBehaviourSuite) TestEnhanceSessionSecurity_WithHTTPS() {
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/test", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
|
|
options := s.sessionManager.EnhanceSessionSecurity(nil, req)
|
|
|
|
s.True(options.Secure)
|
|
s.Equal(http.SameSiteLaxMode, options.SameSite)
|
|
}
|
|
|
|
// TestEnhanceSessionSecurity_MissingUserAgent tests handling of missing User-Agent
|
|
func (s *SessionBehaviourSuite) TestEnhanceSessionSecurity_MissingUserAgent() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
// Explicitly remove User-Agent
|
|
req.Header.Del("User-Agent")
|
|
|
|
options := s.sessionManager.EnhanceSessionSecurity(nil, req)
|
|
|
|
// Should have reduced MaxAge for suspicious requests
|
|
s.NotNil(options)
|
|
}
|
|
|
|
// TestCleanupOldCookies tests cookie cleanup
|
|
func (s *SessionBehaviourSuite) TestCleanupOldCookies() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set("Host", "example.com")
|
|
rw := httptest.NewRecorder()
|
|
|
|
// Add some cookies that match the prefix
|
|
req.AddCookie(&http.Cookie{Name: "_oidc_raczylo_m", Value: "test"})
|
|
req.AddCookie(&http.Cookie{Name: "_oidc_raczylo_a", Value: "test"})
|
|
|
|
// Should not panic
|
|
s.sessionManager.CleanupOldCookies(rw, req)
|
|
}
|
|
|
|
// TestSessionData_DirtyTracking tests dirty flag tracking
|
|
func (s *SessionBehaviourSuite) TestSessionData_DirtyTracking() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Initially not dirty (fresh session from pool)
|
|
s.False(session.IsDirty())
|
|
|
|
// Mark dirty
|
|
session.MarkDirty()
|
|
s.True(session.IsDirty())
|
|
|
|
// Reset should clear dirty flag
|
|
session.Reset()
|
|
s.False(session.IsDirty())
|
|
}
|
|
|
|
// TestSessionData_SetUserIdentifier tests user identifier setter with dirty tracking
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetUserIdentifier() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
session.SetUserIdentifier("test@example.com")
|
|
s.Equal("test@example.com", session.GetUserIdentifier())
|
|
s.True(session.IsDirty())
|
|
}
|
|
|
|
// TestSessionData_SetCSRF tests CSRF setter with dirty tracking
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetCSRF() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set CSRF
|
|
session.SetCSRF("csrf-token-value")
|
|
s.Equal("csrf-token-value", session.GetCSRF())
|
|
s.True(session.IsDirty())
|
|
|
|
// Setting same value should not trigger dirty again
|
|
session.dirty = false
|
|
session.SetCSRF("csrf-token-value")
|
|
s.False(session.IsDirty())
|
|
}
|
|
|
|
// TestSessionData_SetNonce tests nonce setter with dirty tracking
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetNonce() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set nonce
|
|
session.SetNonce("nonce-value")
|
|
s.Equal("nonce-value", session.GetNonce())
|
|
s.True(session.IsDirty())
|
|
}
|
|
|
|
// TestSessionData_SetCodeVerifier tests code verifier setter
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetCodeVerifier() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set code verifier
|
|
session.SetCodeVerifier("pkce-code-verifier")
|
|
s.Equal("pkce-code-verifier", session.GetCodeVerifier())
|
|
s.True(session.IsDirty())
|
|
}
|
|
|
|
// TestSessionData_SetIncomingPath tests incoming path setter
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetIncomingPath() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set incoming path
|
|
session.SetIncomingPath("/original/path?query=value")
|
|
s.Equal("/original/path?query=value", session.GetIncomingPath())
|
|
s.True(session.IsDirty())
|
|
}
|
|
|
|
// TestSessionData_RedirectCount tests redirect count operations
|
|
func (s *SessionBehaviourSuite) TestSessionData_RedirectCount() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Initial count should be 0
|
|
s.Equal(0, session.GetRedirectCount())
|
|
|
|
// Increment
|
|
session.IncrementRedirectCount()
|
|
s.Equal(1, session.GetRedirectCount())
|
|
|
|
session.IncrementRedirectCount()
|
|
s.Equal(2, session.GetRedirectCount())
|
|
|
|
// Reset
|
|
session.ResetRedirectCount()
|
|
s.Equal(0, session.GetRedirectCount())
|
|
}
|
|
|
|
// TestSessionData_SetAccessToken tests access token storage
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetAccessToken() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set a valid opaque access token
|
|
token := "opaque-access-token-with-sufficient-length-for-testing"
|
|
session.SetAccessToken(token)
|
|
|
|
// Get the token back
|
|
retrieved := session.GetAccessToken()
|
|
s.Equal(token, retrieved)
|
|
}
|
|
|
|
// TestSessionData_SetAccessToken_InvalidFormat tests rejection of invalid token format
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetAccessToken_InvalidFormat() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set a token with invalid format (exactly 1 dot is invalid)
|
|
session.SetAccessToken("invalid.token")
|
|
|
|
// Should be rejected
|
|
retrieved := session.GetAccessToken()
|
|
s.Empty(retrieved)
|
|
}
|
|
|
|
// TestSessionData_SetAccessToken_TooShortOpaque tests rejection of too short opaque token
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetAccessToken_TooShortOpaque() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set a very short opaque token (less than 20 chars)
|
|
session.SetAccessToken("short")
|
|
|
|
// Should be rejected
|
|
retrieved := session.GetAccessToken()
|
|
s.Empty(retrieved)
|
|
}
|
|
|
|
// TestSessionData_SetIDToken_ValidJWT tests ID token storage with valid JWT
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetIDToken_ValidJWT() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set a valid JWT format ID token
|
|
token := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.signature"
|
|
session.SetIDToken(token)
|
|
|
|
// The ID token should be stored - verify it directly from the session
|
|
// since GetIDToken uses ChunkManager which may apply additional validation
|
|
storedToken, _ := session.idTokenSession.Values["token"].(string)
|
|
s.NotEmpty(storedToken)
|
|
s.True(session.IsDirty())
|
|
}
|
|
|
|
// TestSessionData_SetIDToken_InvalidFormat tests rejection of invalid ID token format
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetIDToken_InvalidFormat() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set a non-JWT format (ID tokens must be JWT)
|
|
session.SetIDToken("not-a-jwt-token")
|
|
|
|
// Should be rejected
|
|
retrieved := session.GetIDToken()
|
|
s.Empty(retrieved)
|
|
}
|
|
|
|
// TestSessionData_SetRefreshToken tests refresh token storage
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetRefreshToken() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Set refresh token (opaque format is valid)
|
|
token := "refresh-token-opaque-format-value"
|
|
session.SetRefreshToken(token)
|
|
|
|
// Get the token back
|
|
retrieved := session.GetRefreshToken()
|
|
s.Equal(token, retrieved)
|
|
}
|
|
|
|
// TestSessionData_SetRefreshToken_TooLarge tests rejection of too large refresh token
|
|
func (s *SessionBehaviourSuite) TestSessionData_SetRefreshToken_TooLarge() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Create a very large token (over 50KB)
|
|
largeToken := strings.Repeat("x", 60*1024)
|
|
session.SetRefreshToken(largeToken)
|
|
|
|
// Should be rejected
|
|
retrieved := session.GetRefreshToken()
|
|
s.Empty(retrieved)
|
|
}
|
|
|
|
// TestSessionData_GetRefreshTokenIssuedAt tests refresh token issued timestamp
|
|
func (s *SessionBehaviourSuite) TestSessionData_GetRefreshTokenIssuedAt() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Before setting refresh token, issued_at should be zero
|
|
issuedAt := session.GetRefreshTokenIssuedAt()
|
|
s.True(issuedAt.IsZero())
|
|
|
|
// Set refresh token (this sets issued_at)
|
|
session.SetRefreshToken("refresh-token-value-here")
|
|
|
|
// Now issued_at should be set
|
|
issuedAt = session.GetRefreshTokenIssuedAt()
|
|
s.False(issuedAt.IsZero())
|
|
s.True(time.Since(issuedAt) < 5*time.Second) // Should be very recent
|
|
}
|
|
|
|
// TestSessionData_Clear tests session clearing
|
|
func (s *SessionBehaviourSuite) TestSessionData_Clear() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
rw := httptest.NewRecorder()
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
|
|
// Set some data
|
|
err = session.SetAuthenticated(true)
|
|
s.Require().NoError(err)
|
|
session.SetUserIdentifier("test@example.com")
|
|
session.SetCSRF("csrf-token")
|
|
|
|
// Clear session
|
|
err = session.Clear(req, rw)
|
|
s.NoError(err)
|
|
|
|
// After clear, session is returned to pool, so we shouldn't use it
|
|
}
|
|
|
|
// TestSessionData_Save tests session saving
|
|
func (s *SessionBehaviourSuite) TestSessionData_Save() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
rw := httptest.NewRecorder()
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
defer session.returnToPoolSafely()
|
|
|
|
// Modify session
|
|
session.SetUserIdentifier("test@example.com")
|
|
s.True(session.IsDirty())
|
|
|
|
// Save session
|
|
err = session.Save(req, rw)
|
|
s.NoError(err)
|
|
|
|
// After save, dirty flag should be cleared
|
|
s.False(session.IsDirty())
|
|
|
|
// Response should have cookies
|
|
cookies := rw.Result().Cookies()
|
|
s.NotEmpty(cookies)
|
|
}
|
|
|
|
// TestSessionData_ReturnToPool tests returning session to pool
|
|
func (s *SessionBehaviourSuite) TestSessionData_ReturnToPool() {
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
|
|
session, err := s.sessionManager.GetSession(req)
|
|
s.Require().NoError(err)
|
|
|
|
// Initially in use
|
|
s.True(session.inUse)
|
|
|
|
// Return to pool safely
|
|
session.returnToPoolSafely()
|
|
|
|
// Should no longer be in use
|
|
s.False(session.inUse)
|
|
}
|
|
|
|
// TestTokenCompression tests token compression functionality
|
|
func (s *SessionBehaviourSuite) TestTokenCompression() {
|
|
// A typical JWT token that could benefit from compression
|
|
token := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsInN1YiI6InRlc3Qtc3ViamVjdCIsImF1ZCI6InRlc3QtY2xpZW50LWlkIiwiZXhwIjoxNzAyNDE2MDAwLCJpYXQiOjE3MDI0MTI0MDAsIm5vbmNlIjoidGVzdC1ub25jZSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSJ9.signature_data_here"
|
|
|
|
compressed := compressToken(token)
|
|
|
|
// Decompress and verify
|
|
decompressed := decompressToken(compressed)
|
|
s.Equal(token, decompressed)
|
|
}
|
|
|
|
// TestTokenCompression_EmptyToken tests compression of empty token
|
|
func (s *SessionBehaviourSuite) TestTokenCompression_EmptyToken() {
|
|
compressed := compressToken("")
|
|
s.Empty(compressed)
|
|
|
|
decompressed := decompressToken("")
|
|
s.Empty(decompressed)
|
|
}
|
|
|
|
// TestTokenCompression_InvalidFormat tests compression of non-JWT token
|
|
func (s *SessionBehaviourSuite) TestTokenCompression_InvalidFormat() {
|
|
// Token without proper JWT format (wrong number of dots)
|
|
token := "not-a-jwt"
|
|
compressed := compressToken(token)
|
|
|
|
// Should return original (not compressed)
|
|
s.Equal(token, compressed)
|
|
}
|
|
|
|
// TestSplitIntoChunks tests chunk splitting functionality
|
|
func (s *SessionBehaviourSuite) TestSplitIntoChunks() {
|
|
// Test with a string that needs splitting
|
|
data := strings.Repeat("x", 3000)
|
|
chunks := splitIntoChunks(data, 1000)
|
|
|
|
s.Equal(3, len(chunks))
|
|
s.Equal(1000, len(chunks[0]))
|
|
s.Equal(1000, len(chunks[1]))
|
|
s.Equal(1000, len(chunks[2]))
|
|
|
|
// Verify reassembly
|
|
reassembled := strings.Join(chunks, "")
|
|
s.Equal(data, reassembled)
|
|
}
|
|
|
|
// TestSplitIntoChunks_SmallData tests chunk splitting with data smaller than chunk size
|
|
func (s *SessionBehaviourSuite) TestSplitIntoChunks_SmallData() {
|
|
data := "small"
|
|
chunks := splitIntoChunks(data, 1000)
|
|
|
|
s.Equal(1, len(chunks))
|
|
s.Equal(data, chunks[0])
|
|
}
|
|
|
|
// TestValidateChunkSize tests chunk size validation
|
|
func (s *SessionBehaviourSuite) TestValidateChunkSize() {
|
|
// Small chunk should be valid
|
|
s.True(validateChunkSize("small_chunk_data"))
|
|
|
|
// Very large chunk should be invalid
|
|
largeChunk := strings.Repeat("x", 5000)
|
|
s.False(validateChunkSize(largeChunk))
|
|
}
|
|
|
|
// TestIsCorruptionMarker tests corruption marker detection
|
|
func (s *SessionBehaviourSuite) TestIsCorruptionMarker() {
|
|
// Known corruption markers
|
|
s.True(isCorruptionMarker("__CORRUPTION_MARKER_TEST__"))
|
|
s.True(isCorruptionMarker("__INVALID_BASE64_DATA__"))
|
|
s.True(isCorruptionMarker("<<<CORRUPTED>>>"))
|
|
|
|
// Normal data
|
|
s.False(isCorruptionMarker("normal-data"))
|
|
s.False(isCorruptionMarker("eyJhbGciOiJSUzI1NiJ9"))
|
|
s.False(isCorruptionMarker(""))
|
|
|
|
// Data with special characters (in long strings)
|
|
s.True(isCorruptionMarker("long-string-with!special@chars"))
|
|
}
|
|
|
|
// TestSessionManager_Shutdown tests graceful shutdown
|
|
func (s *SessionBehaviourSuite) TestSessionManager_Shutdown() {
|
|
// Create a new session manager for this test
|
|
sm, err := NewSessionManager(
|
|
"test-encryption-key-32-bytes-long!!",
|
|
false,
|
|
"",
|
|
"",
|
|
0,
|
|
s.logger,
|
|
)
|
|
s.Require().NoError(err)
|
|
|
|
// Shutdown should complete without error
|
|
err = sm.Shutdown()
|
|
s.NoError(err)
|
|
|
|
// Second shutdown should also be safe (idempotent)
|
|
err = sm.Shutdown()
|
|
s.NoError(err)
|
|
}
|
|
|
|
// TestCookieNameHelpers tests cookie name helper methods
|
|
func (s *SessionBehaviourSuite) TestCookieNameHelpers() {
|
|
s.Equal("_oidc_raczylo_m", s.sessionManager.mainCookieName())
|
|
s.Equal("_oidc_raczylo_a", s.sessionManager.accessTokenCookieName())
|
|
s.Equal("_oidc_raczylo_r", s.sessionManager.refreshTokenCookieName())
|
|
s.Equal("_oidc_raczylo_id", s.sessionManager.idTokenCookieName())
|
|
}
|
|
|
|
// TestSessionManager_CustomCookiePrefix tests custom cookie prefix
|
|
func (s *SessionBehaviourSuite) TestSessionManager_CustomCookiePrefix() {
|
|
customSM, err := NewSessionManager(
|
|
"test-encryption-key-32-bytes-long!!",
|
|
false,
|
|
"",
|
|
"custom_prefix_",
|
|
0,
|
|
s.logger,
|
|
)
|
|
s.Require().NoError(err)
|
|
defer customSM.Shutdown()
|
|
|
|
s.Equal("custom_prefix_m", customSM.mainCookieName())
|
|
s.Equal("custom_prefix_a", customSM.accessTokenCookieName())
|
|
s.Equal("custom_prefix_r", customSM.refreshTokenCookieName())
|
|
s.Equal("custom_prefix_id", customSM.idTokenCookieName())
|
|
}
|
|
|
|
// TestSessionManager_ShortEncryptionKey tests rejection of short encryption key
|
|
func (s *SessionBehaviourSuite) TestSessionManager_ShortEncryptionKey() {
|
|
_, err := NewSessionManager(
|
|
"short", // Too short
|
|
false,
|
|
"",
|
|
"",
|
|
0,
|
|
s.logger,
|
|
)
|
|
s.Error(err)
|
|
s.Contains(err.Error(), "encryption key must be at least")
|
|
}
|
|
|
|
// TestGenerateSecureRandomString tests secure random string generation
|
|
func (s *SessionBehaviourSuite) TestGenerateSecureRandomString() {
|
|
// Generate two random strings
|
|
str1, err := generateSecureRandomString(32)
|
|
s.NoError(err)
|
|
s.Equal(64, len(str1)) // Hex encoding doubles length
|
|
|
|
str2, err := generateSecureRandomString(32)
|
|
s.NoError(err)
|
|
s.Equal(64, len(str2))
|
|
|
|
// They should be different
|
|
s.NotEqual(str1, str2)
|
|
}
|
|
|
|
// TestConstantTimeStringCompare tests constant-time string comparison
|
|
func (s *SessionBehaviourSuite) TestConstantTimeStringCompare() {
|
|
s.True(constantTimeStringCompare("hello", "hello"))
|
|
s.False(constantTimeStringCompare("hello", "world"))
|
|
s.False(constantTimeStringCompare("hello", "hell"))
|
|
s.False(constantTimeStringCompare("", "hello"))
|
|
s.True(constantTimeStringCompare("", ""))
|
|
}
|
|
|
|
func TestSessionBehaviourSuite(t *testing.T) {
|
|
suite.Run(t, new(SessionBehaviourSuite))
|
|
}
|