mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
Add redis support for distributed caching (#83)
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCookiePrefix tests that custom cookie prefixes work correctly
|
||||
func TestCookiePrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cookiePrefix string
|
||||
wantMain string
|
||||
wantAccess string
|
||||
wantRefresh string
|
||||
wantID string
|
||||
}{
|
||||
{
|
||||
name: "Default prefix",
|
||||
cookiePrefix: "",
|
||||
wantMain: "_oidc_raczylo_m",
|
||||
wantAccess: "_oidc_raczylo_a",
|
||||
wantRefresh: "_oidc_raczylo_r",
|
||||
wantID: "_oidc_raczylo_id",
|
||||
},
|
||||
{
|
||||
name: "Custom prefix",
|
||||
cookiePrefix: "_oidc_myapp_",
|
||||
wantMain: "_oidc_myapp_m",
|
||||
wantAccess: "_oidc_myapp_a",
|
||||
wantRefresh: "_oidc_myapp_r",
|
||||
wantID: "_oidc_myapp_id",
|
||||
},
|
||||
{
|
||||
name: "Custom prefix without underscore suffix",
|
||||
cookiePrefix: "myapp",
|
||||
wantMain: "myappm",
|
||||
wantAccess: "myappa",
|
||||
wantRefresh: "myappr",
|
||||
wantID: "myappid",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
|
||||
sm, err := NewSessionManager(
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
false,
|
||||
"",
|
||||
tt.cookiePrefix,
|
||||
0,
|
||||
logger,
|
||||
chunkManager,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
|
||||
// Test cookie names
|
||||
if got := sm.MainCookieName(); got != tt.wantMain {
|
||||
t.Errorf("MainCookieName() = %q, want %q", got, tt.wantMain)
|
||||
}
|
||||
if got := sm.AccessTokenCookie(); got != tt.wantAccess {
|
||||
t.Errorf("AccessTokenCookie() = %q, want %q", got, tt.wantAccess)
|
||||
}
|
||||
if got := sm.RefreshTokenCookie(); got != tt.wantRefresh {
|
||||
t.Errorf("RefreshTokenCookie() = %q, want %q", got, tt.wantRefresh)
|
||||
}
|
||||
if got := sm.IDTokenCookie(); got != tt.wantID {
|
||||
t.Errorf("IDTokenCookie() = %q, want %q", got, tt.wantID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMultipleInstancesWithDifferentPrefixes tests that multiple session managers
|
||||
// with different prefixes can coexist (addresses issue #87)
|
||||
func TestMultipleInstancesWithDifferentPrefixes(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager1 := &MockChunkManager{}
|
||||
chunkManager2 := &MockChunkManager{}
|
||||
|
||||
// Create two session managers with different prefixes
|
||||
sm1, err := NewSessionManager(
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
false,
|
||||
"example.com",
|
||||
"_oidc_app1_",
|
||||
0,
|
||||
logger,
|
||||
chunkManager1,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager 1: %v", err)
|
||||
}
|
||||
|
||||
sm2, err := NewSessionManager(
|
||||
"fedcba9876543210fedcba9876543210fedcba9876543210", // Different encryption key
|
||||
false,
|
||||
"example.com",
|
||||
"_oidc_app2_",
|
||||
0,
|
||||
logger,
|
||||
chunkManager2,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager 2: %v", err)
|
||||
}
|
||||
|
||||
// Verify they have different cookie names
|
||||
if sm1.MainCookieName() == sm2.MainCookieName() {
|
||||
t.Error("Expected different main cookie names for different instances")
|
||||
}
|
||||
|
||||
// Verify cookie name patterns
|
||||
expectedPrefix1 := "_oidc_app1_"
|
||||
expectedPrefix2 := "_oidc_app2_"
|
||||
|
||||
if sm1.MainCookieName() != expectedPrefix1+"m" {
|
||||
t.Errorf("Expected main cookie name %s, got %s", expectedPrefix1+"m", sm1.MainCookieName())
|
||||
}
|
||||
|
||||
if sm2.MainCookieName() != expectedPrefix2+"m" {
|
||||
t.Errorf("Expected main cookie name %s, got %s", expectedPrefix2+"m", sm2.MainCookieName())
|
||||
}
|
||||
|
||||
t.Log("✓ Session isolation verified: Different cookie prefixes prevent session sharing")
|
||||
}
|
||||
@@ -18,14 +18,16 @@ const (
|
||||
|
||||
// SessionManager handles session creation, management and cleanup
|
||||
type SessionManager struct {
|
||||
sessionPool sync.Pool
|
||||
store sessions.Store
|
||||
logger Logger
|
||||
chunkManager ChunkManager
|
||||
cookieDomain string
|
||||
cleanupMutex sync.RWMutex
|
||||
forceHTTPS bool
|
||||
cleanupDone bool
|
||||
sessionPool sync.Pool
|
||||
store sessions.Store
|
||||
logger Logger
|
||||
chunkManager ChunkManager
|
||||
cookieDomain string
|
||||
cookiePrefix string // Prefix for cookie names (default: "_oidc_raczylo_")
|
||||
sessionMaxAge time.Duration // Maximum session age (default: 24 hours)
|
||||
cleanupMutex sync.RWMutex
|
||||
forceHTTPS bool
|
||||
cleanupDone bool
|
||||
}
|
||||
|
||||
// Logger interface for dependency injection
|
||||
@@ -69,17 +71,29 @@ type SessionData interface {
|
||||
// NewSessionManager creates a new SessionManager instance with secure defaults.
|
||||
// It initializes the cookie store with encryption, sets up session pooling,
|
||||
// and configures chunk management for large tokens.
|
||||
func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain string, logger Logger, chunkManager ChunkManager) (*SessionManager, error) {
|
||||
func NewSessionManager(encryptionKey string, forceHTTPS bool, cookieDomain string, cookiePrefix string, sessionMaxAge time.Duration, logger Logger, chunkManager ChunkManager) (*SessionManager, error) {
|
||||
if len(encryptionKey) < minEncryptionKeyLength {
|
||||
return nil, fmt.Errorf("encryption key must be at least %d bytes long", minEncryptionKeyLength)
|
||||
}
|
||||
|
||||
// Set default cookie prefix if not provided
|
||||
if cookiePrefix == "" {
|
||||
cookiePrefix = "_oidc_raczylo_"
|
||||
}
|
||||
|
||||
// Set default session max age if not provided (24 hours for backward compatibility)
|
||||
if sessionMaxAge == 0 {
|
||||
sessionMaxAge = absoluteSessionTimeout
|
||||
}
|
||||
|
||||
sm := &SessionManager{
|
||||
store: sessions.NewCookieStore([]byte(encryptionKey)),
|
||||
forceHTTPS: forceHTTPS,
|
||||
cookieDomain: cookieDomain,
|
||||
logger: logger,
|
||||
chunkManager: chunkManager,
|
||||
store: sessions.NewCookieStore([]byte(encryptionKey)),
|
||||
forceHTTPS: forceHTTPS,
|
||||
cookieDomain: cookieDomain,
|
||||
cookiePrefix: cookiePrefix,
|
||||
sessionMaxAge: sessionMaxAge,
|
||||
logger: logger,
|
||||
chunkManager: chunkManager,
|
||||
}
|
||||
|
||||
sm.sessionPool.New = func() interface{} {
|
||||
@@ -114,7 +128,7 @@ func (sm *SessionManager) initializeSession(sessionData SessionData, r *http.Req
|
||||
sessionData.SetManager(sm)
|
||||
|
||||
// Load session data from cookies
|
||||
session, err := sm.store.Get(r, MainCookieName())
|
||||
session, err := sm.store.Get(r, sm.MainCookieName())
|
||||
if err != nil {
|
||||
sm.logger.Debugf("Error getting main session: %v", err)
|
||||
return nil // Not a fatal error, will create new session
|
||||
@@ -315,14 +329,21 @@ func (sm *SessionManager) getSessionOptions(isSecure bool) *sessions.Options {
|
||||
return &sessions.Options{
|
||||
Path: "/",
|
||||
Domain: sm.cookieDomain,
|
||||
MaxAge: int(absoluteSessionTimeout.Seconds()),
|
||||
MaxAge: int(sm.sessionMaxAge.Seconds()),
|
||||
Secure: isSecure,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
}
|
||||
|
||||
// Cookie name functions
|
||||
// Cookie name methods - these now use the configurable prefix
|
||||
func (sm *SessionManager) MainCookieName() string { return sm.cookiePrefix + "m" }
|
||||
func (sm *SessionManager) AccessTokenCookie() string { return sm.cookiePrefix + "a" }
|
||||
func (sm *SessionManager) RefreshTokenCookie() string { return sm.cookiePrefix + "r" }
|
||||
func (sm *SessionManager) IDTokenCookie() string { return sm.cookiePrefix + "id" }
|
||||
|
||||
// Package-level functions for backward compatibility (use default prefix)
|
||||
// These are deprecated and will be removed in a future version
|
||||
func MainCookieName() string { return "_oidc_raczylo_m" }
|
||||
func AccessTokenCookie() string { return "_oidc_raczylo_a" }
|
||||
func RefreshTokenCookie() string { return "_oidc_raczylo_r" }
|
||||
|
||||
@@ -165,7 +165,7 @@ func TestSessionManagerCreation(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
|
||||
sm, err := NewSessionManager(tt.encryptionKey, false, "", logger, chunkManager)
|
||||
sm, err := NewSessionManager(tt.encryptionKey, false, "", "", 0, logger, chunkManager)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
@@ -200,7 +200,7 @@ func TestSessionManagerCreation(t *testing.T) {
|
||||
func TestSessionManagerPoolBehavior(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager)
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -291,7 +291,7 @@ func TestSessionManagerPoolBehavior(t *testing.T) {
|
||||
func TestSessionManagerErrorHandling(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager)
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -390,7 +390,7 @@ func TestSessionManagerCleanup(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
mockChunkManager := &MockChunkManager{}
|
||||
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, mockChunkManager)
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", 0, logger, mockChunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -458,7 +458,7 @@ func TestSessionManagerHTTPSBehavior(t *testing.T) {
|
||||
chunkManager := &MockChunkManager{}
|
||||
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
tt.forceHTTPS, "", logger, chunkManager)
|
||||
tt.forceHTTPS, "", "", 0, logger, chunkManager)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
@@ -520,7 +520,7 @@ func TestSessionManagerCookieDomain(t *testing.T) {
|
||||
chunkManager := &MockChunkManager{}
|
||||
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
false, tt.cookieDomain, logger, chunkManager)
|
||||
false, tt.cookieDomain, "", 0, logger, chunkManager)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for %s: %v", tt.description, err)
|
||||
@@ -549,7 +549,7 @@ func BenchmarkSessionManagerCreation(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
sm, err := NewSessionManager(encryptionKey, false, "", logger, chunkManager)
|
||||
sm, err := NewSessionManager(encryptionKey, false, "", "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -561,7 +561,7 @@ func BenchmarkSessionManagerCreation(b *testing.B) {
|
||||
func BenchmarkSessionManagerGetSession(b *testing.B) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager)
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -599,7 +599,7 @@ func minInt(a, b int) int {
|
||||
func TestValidateSessionHealth(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager)
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -660,7 +660,7 @@ func TestValidateSessionHealth(t *testing.T) {
|
||||
func TestValidateTokenFormat(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager)
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -727,7 +727,7 @@ func TestValidateTokenFormat(t *testing.T) {
|
||||
func TestDetectSessionTampering(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", logger, chunkManager)
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef", false, "", "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -812,7 +812,7 @@ func TestGetSessionMetrics(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
tt.forceHTTPS, tt.cookieDomain, logger, chunkManager)
|
||||
tt.forceHTTPS, tt.cookieDomain, "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -898,7 +898,7 @@ func TestShouldUseSecureCookies(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
tt.forceHTTPS, "", logger, chunkManager)
|
||||
tt.forceHTTPS, "", "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
@@ -940,7 +940,7 @@ func TestGetSessionOptions(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
chunkManager := &MockChunkManager{}
|
||||
sm, err := NewSessionManager("0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
false, tt.cookieDomain, logger, chunkManager)
|
||||
false, tt.cookieDomain, "", 0, logger, chunkManager)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session manager: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user