mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-07 22:53:58 +00:00
Compare commits
5 Commits
v0.4.3-beta.2
...
v0.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 8512ad6d68 | |||
| 5aa838c669 | |||
| 6f359e5ef1 | |||
| bd18d6041c | |||
| 74c620ad51 |
@@ -24,13 +24,24 @@ type Cache struct {
|
||||
// mutex protects concurrent access to the items map
|
||||
// Use RLock/RUnlock for reads and Lock/Unlock for writes
|
||||
mutex sync.RWMutex
|
||||
|
||||
// maxSize is the maximum number of items allowed in the cache
|
||||
maxSize int
|
||||
|
||||
// accessList maintains the order of item access for eviction
|
||||
accessList []string
|
||||
}
|
||||
|
||||
// DefaultMaxSize is the default maximum number of items in the cache
|
||||
const DefaultMaxSize = 1000
|
||||
|
||||
// NewCache creates a new empty cache instance.
|
||||
// The cache is immediately ready for use and is thread-safe.
|
||||
func NewCache() *Cache {
|
||||
return &Cache{
|
||||
items: make(map[string]CacheItem),
|
||||
items: make(map[string]CacheItem),
|
||||
maxSize: DefaultMaxSize,
|
||||
accessList: make([]string, 0, DefaultMaxSize),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +54,27 @@ func NewCache() *Cache {
|
||||
func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
// If key exists, update it
|
||||
if _, exists := c.items[key]; exists {
|
||||
c.items[key] = CacheItem{
|
||||
Value: value,
|
||||
ExpiresAt: time.Now().Add(expiration),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If cache is full, remove oldest item
|
||||
if len(c.items) >= c.maxSize {
|
||||
c.evictOldest()
|
||||
}
|
||||
|
||||
// Add new item
|
||||
c.items[key] = CacheItem{
|
||||
Value: value,
|
||||
ExpiresAt: time.Now().Add(expiration),
|
||||
}
|
||||
c.accessList = append(c.accessList, key)
|
||||
}
|
||||
|
||||
// Get retrieves an item from the cache if it exists and hasn't expired.
|
||||
@@ -58,15 +86,25 @@ func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
|
||||
// Thread-safe: Uses read locking to ensure safe concurrent access.
|
||||
func (c *Cache) Get(key string) (interface{}, bool) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
item, found := c.items[key]
|
||||
c.mutex.RUnlock()
|
||||
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if time.Now().After(item.ExpiresAt) {
|
||||
delete(c.items, key)
|
||||
c.mutex.Lock()
|
||||
c.removeItem(key)
|
||||
c.mutex.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Update access order
|
||||
c.mutex.Lock()
|
||||
c.updateAccessOrder(key)
|
||||
c.mutex.Unlock()
|
||||
|
||||
return item.Value, true
|
||||
}
|
||||
|
||||
@@ -86,10 +124,46 @@ func (c *Cache) Delete(key string) {
|
||||
func (c *Cache) Cleanup() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for key, item := range c.items {
|
||||
if now.After(item.ExpiresAt) {
|
||||
var newAccessList []string
|
||||
|
||||
for _, key := range c.accessList {
|
||||
if item, exists := c.items[key]; exists && !now.After(item.ExpiresAt) {
|
||||
newAccessList = append(newAccessList, key)
|
||||
} else {
|
||||
delete(c.items, key)
|
||||
}
|
||||
}
|
||||
|
||||
c.accessList = newAccessList
|
||||
}
|
||||
|
||||
// evictOldest removes the least recently used item from the cache
|
||||
func (c *Cache) evictOldest() {
|
||||
if len(c.accessList) > 0 {
|
||||
oldest := c.accessList[0]
|
||||
c.removeItem(oldest)
|
||||
}
|
||||
}
|
||||
|
||||
// removeItem removes an item from both the cache and access list
|
||||
func (c *Cache) removeItem(key string) {
|
||||
delete(c.items, key)
|
||||
for i, k := range c.accessList {
|
||||
if k == key {
|
||||
c.accessList = append(c.accessList[:i], c.accessList[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateAccessOrder moves the accessed key to the end of the access list
|
||||
func (c *Cache) updateAccessOrder(key string) {
|
||||
for i, k := range c.accessList {
|
||||
if k == key {
|
||||
c.accessList = append(append(c.accessList[:i], c.accessList[i+1:]...), key)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,17 +180,19 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
Timeout: 15 * time.Second, // Reduced timeout
|
||||
KeepAlive: 15 * time.Second, // Reduced keepalive
|
||||
}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
},
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSHandshakeTimeout: 5 * time.Second, // Reduced from 10s
|
||||
ExpectContinueTimeout: 0,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
MaxIdleConns: 30, // Reduced from 100
|
||||
MaxIdleConnsPerHost: 10, // Reduced from 100
|
||||
IdleConnTimeout: 30 * time.Second, // Reduced from 90s
|
||||
DisableKeepAlives: false, // Enable connection reuse
|
||||
MaxConnsPerHost: 50, // Limit max connections
|
||||
}
|
||||
|
||||
var httpClient *http.Client
|
||||
@@ -198,7 +200,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||
httpClient = config.HTTPClient
|
||||
} else {
|
||||
httpClient = &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Timeout: time.Second * 15, // Reduced timeout
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
+122
-32
@@ -1,9 +1,14 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
@@ -36,6 +41,40 @@ const (
|
||||
maxCookieSize = 2000
|
||||
)
|
||||
|
||||
// compressToken compresses a token using gzip and base64 encodes it
|
||||
func compressToken(token string) string {
|
||||
var b bytes.Buffer
|
||||
gz := gzip.NewWriter(&b)
|
||||
if _, err := gz.Write([]byte(token)); err != nil {
|
||||
return token // fallback to uncompressed on error
|
||||
}
|
||||
if err := gz.Close(); err != nil {
|
||||
return token
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b.Bytes())
|
||||
}
|
||||
|
||||
// decompressToken decompresses a base64 encoded gzipped token
|
||||
func decompressToken(compressed string) string {
|
||||
data, err := base64.StdEncoding.DecodeString(compressed)
|
||||
if err != nil {
|
||||
return compressed // return as-is if not base64
|
||||
}
|
||||
|
||||
gz, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return compressed
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(gz)
|
||||
if err != nil {
|
||||
return compressed
|
||||
}
|
||||
|
||||
return string(decompressed)
|
||||
}
|
||||
|
||||
// SessionManager handles the management of multiple session cookies for OIDC authentication.
|
||||
// It provides functionality for storing and retrieving authentication state, tokens,
|
||||
// and other session-related data across multiple cookies to handle large tokens.
|
||||
@@ -48,6 +87,9 @@ type SessionManager struct {
|
||||
|
||||
// logger provides structured logging capabilities
|
||||
logger *Logger
|
||||
|
||||
// sessionPool is a sync.Pool for reusing SessionData objects
|
||||
sessionPool sync.Pool
|
||||
}
|
||||
|
||||
// NewSessionManager creates a new session manager with the specified configuration.
|
||||
@@ -57,11 +99,22 @@ type SessionManager struct {
|
||||
// - logger: Logger instance for recording session-related events
|
||||
// The manager handles session creation, storage, and cookie security settings.
|
||||
func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) *SessionManager {
|
||||
return &SessionManager{
|
||||
sm := &SessionManager{
|
||||
store: sessions.NewCookieStore([]byte(encryptionKey)),
|
||||
forceHTTPS: forceHTTPS,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Initialize session pool
|
||||
sm.sessionPool.New = func() interface{} {
|
||||
return &SessionData{
|
||||
manager: sm,
|
||||
accessTokenChunks: make(map[int]*sessions.Session),
|
||||
refreshTokenChunks: make(map[int]*sessions.Session),
|
||||
}
|
||||
}
|
||||
|
||||
return sm
|
||||
}
|
||||
|
||||
// getSessionOptions returns secure session options configured for the current request.
|
||||
@@ -87,33 +140,40 @@ func (sm *SessionManager) getSessionOptions(isSecure bool) *sessions.Options {
|
||||
// and combines them into a single SessionData structure for easy access.
|
||||
// Returns an error if any session component cannot be loaded.
|
||||
func (sm *SessionManager) GetSession(r *http.Request) (*SessionData, error) {
|
||||
mainSession, err := sm.store.Get(r, mainCookieName)
|
||||
// Get session from pool
|
||||
sessionData := sm.sessionPool.Get().(*SessionData)
|
||||
sessionData.request = r
|
||||
|
||||
var err error
|
||||
sessionData.mainSession, err = sm.store.Get(r, mainCookieName)
|
||||
if err != nil {
|
||||
sm.sessionPool.Put(sessionData)
|
||||
return nil, fmt.Errorf("failed to get main session: %w", err)
|
||||
}
|
||||
|
||||
accessSession, err := sm.store.Get(r, accessTokenCookie)
|
||||
sessionData.accessSession, err = sm.store.Get(r, accessTokenCookie)
|
||||
if err != nil {
|
||||
sm.sessionPool.Put(sessionData)
|
||||
return nil, fmt.Errorf("failed to get access token session: %w", err)
|
||||
}
|
||||
|
||||
refreshSession, err := sm.store.Get(r, refreshTokenCookie)
|
||||
sessionData.refreshSession, err = sm.store.Get(r, refreshTokenCookie)
|
||||
if err != nil {
|
||||
sm.sessionPool.Put(sessionData)
|
||||
return nil, fmt.Errorf("failed to get refresh token session: %w", err)
|
||||
}
|
||||
|
||||
sessionData := &SessionData{
|
||||
manager: sm,
|
||||
request: r,
|
||||
mainSession: mainSession,
|
||||
accessSession: accessSession,
|
||||
refreshSession: refreshSession,
|
||||
// Clear and reuse chunk maps
|
||||
for k := range sessionData.accessTokenChunks {
|
||||
delete(sessionData.accessTokenChunks, k)
|
||||
}
|
||||
for k := range sessionData.refreshTokenChunks {
|
||||
delete(sessionData.refreshTokenChunks, k)
|
||||
}
|
||||
|
||||
// Retrieve chunked access token sessions
|
||||
sessionData.accessTokenChunks = sm.getTokenChunkSessions(r, accessTokenCookie)
|
||||
// Retrieve chunked refresh token sessions
|
||||
sessionData.refreshTokenChunks = sm.getTokenChunkSessions(r, refreshTokenCookie)
|
||||
// Retrieve chunked token sessions
|
||||
sm.getTokenChunkSessions(r, accessTokenCookie, sessionData.accessTokenChunks)
|
||||
sm.getTokenChunkSessions(r, refreshTokenCookie, sessionData.refreshTokenChunks)
|
||||
|
||||
return sessionData, nil
|
||||
}
|
||||
@@ -122,10 +182,8 @@ func (sm *SessionManager) GetSession(r *http.Request) (*SessionData, error) {
|
||||
// Parameters:
|
||||
// - r: The HTTP request
|
||||
// - baseName: The base name for the token's session cookies
|
||||
// Returns a map of chunk index to session, used for handling large tokens
|
||||
// that exceed single cookie size limits.
|
||||
func (sm *SessionManager) getTokenChunkSessions(r *http.Request, baseName string) map[int]*sessions.Session {
|
||||
chunks := make(map[int]*sessions.Session)
|
||||
// - chunks: Map to store the chunks in
|
||||
func (sm *SessionManager) getTokenChunkSessions(r *http.Request, baseName string, chunks map[int]*sessions.Session) {
|
||||
for i := 0; ; i++ {
|
||||
sessionName := fmt.Sprintf("%s_%d", baseName, i)
|
||||
session, err := sm.store.Get(r, sessionName)
|
||||
@@ -135,7 +193,6 @@ func (sm *SessionManager) getTokenChunkSessions(r *http.Request, baseName string
|
||||
}
|
||||
chunks[i] = session
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
// SessionData holds all session information for an authenticated user.
|
||||
@@ -237,7 +294,12 @@ func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
||||
sd.clearTokenChunks(r, sd.accessTokenChunks)
|
||||
sd.clearTokenChunks(r, sd.refreshTokenChunks)
|
||||
|
||||
return sd.Save(r, w)
|
||||
err := sd.Save(r, w)
|
||||
|
||||
// Return session to pool
|
||||
sd.manager.sessionPool.Put(sd)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// clearTokenChunks removes all session chunks for a given token type.
|
||||
@@ -273,6 +335,10 @@ func (sd *SessionData) SetAuthenticated(value bool) {
|
||||
func (sd *SessionData) GetAccessToken() string {
|
||||
token, _ := sd.accessSession.Values["token"].(string)
|
||||
if token != "" {
|
||||
compressed, _ := sd.accessSession.Values["compressed"].(bool)
|
||||
if compressed {
|
||||
return decompressToken(token)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -291,11 +357,16 @@ func (sd *SessionData) GetAccessToken() string {
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
|
||||
return strings.Join(chunks, "")
|
||||
token = strings.Join(chunks, "")
|
||||
compressed, _ := sd.accessSession.Values["compressed"].(bool)
|
||||
if compressed {
|
||||
return decompressToken(token)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// SetAccessToken stores the access token in the session.
|
||||
// If the token exceeds maxCookieSize, it is automatically split into
|
||||
// If the token exceeds maxCookieSize, it is automatically compressed and split into
|
||||
// multiple cookie chunks to handle large tokens while staying within
|
||||
// browser cookie size limits. Any existing token or chunks are cleared
|
||||
// before setting the new token.
|
||||
@@ -304,12 +375,17 @@ func (sd *SessionData) SetAccessToken(token string) {
|
||||
sd.clearTokenChunks(sd.request, sd.accessTokenChunks)
|
||||
sd.accessTokenChunks = make(map[int]*sessions.Session)
|
||||
|
||||
if len(token) <= maxCookieSize {
|
||||
sd.accessSession.Values["token"] = token
|
||||
// Compress token
|
||||
compressed := compressToken(token)
|
||||
|
||||
if len(compressed) <= maxCookieSize {
|
||||
sd.accessSession.Values["token"] = compressed
|
||||
sd.accessSession.Values["compressed"] = true
|
||||
} else {
|
||||
// Split token into chunks
|
||||
// Split compressed token into chunks
|
||||
sd.accessSession.Values["token"] = ""
|
||||
chunks := splitIntoChunks(token, maxCookieSize)
|
||||
sd.accessSession.Values["compressed"] = true
|
||||
chunks := splitIntoChunks(compressed, maxCookieSize)
|
||||
for i, chunk := range chunks {
|
||||
sessionName := fmt.Sprintf("%s_%d", accessTokenCookie, i)
|
||||
session, _ := sd.manager.store.Get(sd.request, sessionName)
|
||||
@@ -326,6 +402,10 @@ func (sd *SessionData) SetAccessToken(token string) {
|
||||
func (sd *SessionData) GetRefreshToken() string {
|
||||
token, _ := sd.refreshSession.Values["token"].(string)
|
||||
if token != "" {
|
||||
compressed, _ := sd.refreshSession.Values["compressed"].(bool)
|
||||
if compressed {
|
||||
return decompressToken(token)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -344,11 +424,16 @@ func (sd *SessionData) GetRefreshToken() string {
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
|
||||
return strings.Join(chunks, "")
|
||||
token = strings.Join(chunks, "")
|
||||
compressed, _ := sd.refreshSession.Values["compressed"].(bool)
|
||||
if compressed {
|
||||
return decompressToken(token)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// SetRefreshToken stores the refresh token in the session.
|
||||
// If the token exceeds maxCookieSize, it is automatically split into
|
||||
// If the token exceeds maxCookieSize, it is automatically compressed and split into
|
||||
// multiple cookie chunks to handle large tokens while staying within
|
||||
// browser cookie size limits. Any existing token or chunks are cleared
|
||||
// before setting the new token.
|
||||
@@ -357,12 +442,17 @@ func (sd *SessionData) SetRefreshToken(token string) {
|
||||
sd.clearTokenChunks(sd.request, sd.refreshTokenChunks)
|
||||
sd.refreshTokenChunks = make(map[int]*sessions.Session)
|
||||
|
||||
if len(token) <= maxCookieSize {
|
||||
sd.refreshSession.Values["token"] = token
|
||||
// Compress token
|
||||
compressed := compressToken(token)
|
||||
|
||||
if len(compressed) <= maxCookieSize {
|
||||
sd.refreshSession.Values["token"] = compressed
|
||||
sd.refreshSession.Values["compressed"] = true
|
||||
} else {
|
||||
// Split token into chunks
|
||||
// Split compressed token into chunks
|
||||
sd.refreshSession.Values["token"] = ""
|
||||
chunks := splitIntoChunks(token, maxCookieSize)
|
||||
sd.refreshSession.Values["compressed"] = true
|
||||
chunks := splitIntoChunks(compressed, maxCookieSize)
|
||||
for i, chunk := range chunks {
|
||||
sessionName := fmt.Sprintf("%s_%d", refreshTokenCookie, i)
|
||||
session, _ := sd.manager.store.Get(sd.request, sessionName)
|
||||
|
||||
+194
-55
@@ -1,57 +1,140 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize random seed
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// generateRandomString creates a random string of specified length
|
||||
func generateRandomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// TestTokenCompression tests the token compression functionality
|
||||
func TestTokenCompression(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
wantSize int // Expected size after compression (approximate)
|
||||
}{
|
||||
{
|
||||
name: "Short token",
|
||||
token: "shorttoken",
|
||||
wantSize: 50, // Base64 encoded gzip has overhead for small content
|
||||
},
|
||||
{
|
||||
name: "Repeating content",
|
||||
token: strings.Repeat("abcdef", 1000),
|
||||
wantSize: 100, // Should compress well due to repetition
|
||||
},
|
||||
{
|
||||
name: "Random content",
|
||||
token: generateRandomString(1000),
|
||||
wantSize: 2000, // Random content won't compress much
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
compressed := compressToken(tt.token)
|
||||
decompressed := decompressToken(compressed)
|
||||
|
||||
// Only verify compression ratio for non-short tokens
|
||||
if len(tt.token) > 100 {
|
||||
compressionRatio := float64(len(compressed)) / float64(len(tt.token))
|
||||
t.Logf("Compression ratio for %s: %.2f", tt.name, compressionRatio)
|
||||
|
||||
if compressionRatio > 1.1 { // Allow up to 10% size increase
|
||||
t.Errorf("Compression increased size too much: original=%d, compressed=%d, ratio=%.2f",
|
||||
len(tt.token), len(compressed), compressionRatio)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify decompression restores original
|
||||
if decompressed != tt.token {
|
||||
t.Error("Decompression failed to restore original token")
|
||||
}
|
||||
|
||||
// Verify approximate compression ratio
|
||||
if len(compressed) > tt.wantSize*2 {
|
||||
t.Errorf("Compression ratio worse than expected: got=%d, want<%d", len(compressed), tt.wantSize*2)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSessionManager tests the SessionManager functionality
|
||||
func TestSessionManager(t *testing.T) {
|
||||
ts := &TestSuite{t: t}
|
||||
ts.Setup()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
authenticated bool
|
||||
email string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
expectedCookieCount int
|
||||
name string
|
||||
authenticated bool
|
||||
email string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
expectedCookieCount int
|
||||
wantCompressed bool // Whether tokens should be compressed
|
||||
}{
|
||||
{
|
||||
name: "Short tokens",
|
||||
authenticated: true,
|
||||
email: "test@example.com",
|
||||
accessToken: "shortaccesstoken",
|
||||
refreshToken: "shortrefreshtoken",
|
||||
expectedCookieCount: 3, // main, access, refresh
|
||||
},
|
||||
{
|
||||
name: "Long tokens exceeding 4096 bytes",
|
||||
authenticated: true,
|
||||
email: "test@example.com",
|
||||
accessToken: strings.Repeat("x", 5000),
|
||||
refreshToken: strings.Repeat("y", 6000),
|
||||
// Recalculate expected cookies based on new maxCookieSize
|
||||
expectedCookieCount: calculateExpectedCookieCount(strings.Repeat("x", 5000), strings.Repeat("y", 6000)),
|
||||
},
|
||||
{
|
||||
name: "REALLY long tokens, exceeding 25000 bytes",
|
||||
authenticated: true,
|
||||
email: "test@example.com",
|
||||
accessToken: strings.Repeat("x", 25000),
|
||||
refreshToken: strings.Repeat("y", 25000),
|
||||
expectedCookieCount: calculateExpectedCookieCount(strings.Repeat("x", 25000), strings.Repeat("y", 25000)),
|
||||
},
|
||||
{
|
||||
name: "Unauthenticated session",
|
||||
authenticated: false,
|
||||
email: "",
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
expectedCookieCount: 3, // main, access, refresh
|
||||
},
|
||||
{
|
||||
name: "Short tokens",
|
||||
authenticated: true,
|
||||
email: "test@example.com",
|
||||
accessToken: "shortaccesstoken",
|
||||
refreshToken: "shortrefreshtoken",
|
||||
expectedCookieCount: 3, // main, access, refresh
|
||||
wantCompressed: true,
|
||||
},
|
||||
{
|
||||
name: "Long tokens exceeding 4096 bytes",
|
||||
authenticated: true,
|
||||
email: "test@example.com",
|
||||
accessToken: strings.Repeat("x", 5000),
|
||||
refreshToken: strings.Repeat("y", 6000),
|
||||
expectedCookieCount: calculateExpectedCookieCount(strings.Repeat("x", 5000), strings.Repeat("y", 6000)),
|
||||
wantCompressed: true,
|
||||
},
|
||||
{
|
||||
name: "REALLY long tokens, exceeding 25000 bytes",
|
||||
authenticated: true,
|
||||
email: "test@example.com",
|
||||
accessToken: strings.Repeat("x", 25000),
|
||||
refreshToken: strings.Repeat("y", 25000),
|
||||
expectedCookieCount: calculateExpectedCookieCount(strings.Repeat("x", 25000), strings.Repeat("y", 25000)),
|
||||
wantCompressed: true,
|
||||
},
|
||||
{
|
||||
name: "Unauthenticated session",
|
||||
authenticated: false,
|
||||
email: "",
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
expectedCookieCount: 3, // main, access, refresh
|
||||
wantCompressed: false,
|
||||
},
|
||||
{
|
||||
name: "Random content tokens",
|
||||
authenticated: true,
|
||||
email: "test@example.com",
|
||||
accessToken: generateRandomString(5000),
|
||||
refreshToken: generateRandomString(5000),
|
||||
expectedCookieCount: calculateExpectedCookieCount(generateRandomString(5000), generateRandomString(5000)),
|
||||
wantCompressed: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -76,10 +159,45 @@ func TestSessionManager(t *testing.T) {
|
||||
t.Fatalf("Failed to save session: %v", err)
|
||||
}
|
||||
|
||||
// Verify cookies are set
|
||||
// Verify cookies are set and compression is used when appropriate
|
||||
cookies := rr.Result().Cookies()
|
||||
if len(cookies) != tc.expectedCookieCount {
|
||||
t.Errorf("Expected %d cookies, got %d", tc.expectedCookieCount, len(cookies))
|
||||
t.Errorf("Expected %d cookies, got %d", tc.expectedCookieCount, len(cookies))
|
||||
}
|
||||
|
||||
// Verify compression is working by checking token sizes
|
||||
for _, cookie := range cookies {
|
||||
if strings.Contains(cookie.Name, accessTokenCookie) {
|
||||
// Get original and stored sizes
|
||||
originalSize := len(tc.accessToken)
|
||||
storedSize := len(cookie.Value)
|
||||
|
||||
if originalSize > 100 && tc.wantCompressed {
|
||||
// For large tokens, verify some compression occurred
|
||||
compressionRatio := float64(storedSize) / float64(originalSize)
|
||||
t.Logf("Access token compression ratio: %.2f (original: %d, stored: %d)",
|
||||
compressionRatio, originalSize, storedSize)
|
||||
|
||||
if compressionRatio > 0.9 { // Allow some overhead, but should see compression
|
||||
t.Errorf("Expected compression for large token in cookie %s (ratio: %.2f)",
|
||||
cookie.Name, compressionRatio)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(cookie.Name, refreshTokenCookie) {
|
||||
originalSize := len(tc.refreshToken)
|
||||
storedSize := len(cookie.Value)
|
||||
|
||||
if originalSize > 100 && tc.wantCompressed {
|
||||
compressionRatio := float64(storedSize) / float64(originalSize)
|
||||
t.Logf("Refresh token compression ratio: %.2f (original: %d, stored: %d)",
|
||||
compressionRatio, originalSize, storedSize)
|
||||
|
||||
if compressionRatio > 0.9 {
|
||||
t.Errorf("Expected compression for large token in cookie %s (ratio: %.2f)",
|
||||
cookie.Name, compressionRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new request with the cookies
|
||||
@@ -91,20 +209,27 @@ func TestSessionManager(t *testing.T) {
|
||||
// Get the session again and verify values
|
||||
newSession, err := ts.sessionManager.GetSession(newReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get new session: %v", err)
|
||||
t.Fatalf("Failed to get new session: %v", err)
|
||||
}
|
||||
|
||||
// Verify session values
|
||||
if newSession.GetAuthenticated() != tc.authenticated {
|
||||
t.Errorf("Authentication status not preserved")
|
||||
t.Errorf("Authentication status not preserved")
|
||||
}
|
||||
if email := newSession.GetEmail(); email != tc.email {
|
||||
t.Errorf("Expected email %s, got %s", tc.email, email)
|
||||
t.Errorf("Expected email %s, got %s", tc.email, email)
|
||||
}
|
||||
if token := newSession.GetAccessToken(); token != tc.accessToken {
|
||||
t.Errorf("Access token not preserved")
|
||||
t.Errorf("Access token not preserved: got len=%d, want len=%d", len(token), len(tc.accessToken))
|
||||
}
|
||||
if token := newSession.GetRefreshToken(); token != tc.refreshToken {
|
||||
t.Errorf("Refresh token not preserved")
|
||||
t.Errorf("Refresh token not preserved: got len=%d, want len=%d", len(token), len(tc.refreshToken))
|
||||
}
|
||||
|
||||
// Verify session pooling by checking if the session is reused
|
||||
session2, _ := ts.sessionManager.GetSession(newReq)
|
||||
if session2 == newSession {
|
||||
t.Error("Session not properly pooled")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -113,17 +238,31 @@ func TestSessionManager(t *testing.T) {
|
||||
func calculateExpectedCookieCount(accessToken, refreshToken string) int {
|
||||
count := 3 // main, access, refresh
|
||||
|
||||
// Calculate number of chunks for access token
|
||||
accessChunks := len(splitIntoChunks(accessToken, maxCookieSize))
|
||||
if accessChunks > 1 {
|
||||
count += accessChunks
|
||||
// Helper to calculate chunks for compressed token
|
||||
calculateChunks := func(token string) int {
|
||||
// Compress token (matching the actual implementation)
|
||||
compressed := compressToken(token)
|
||||
|
||||
// If compressed token fits in one cookie, no additional chunks needed
|
||||
if len(compressed) <= maxCookieSize {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate chunks needed for compressed token
|
||||
return len(splitIntoChunks(compressed, maxCookieSize))
|
||||
}
|
||||
|
||||
// Calculate number of chunks for refresh token
|
||||
refreshChunks := len(splitIntoChunks(refreshToken, maxCookieSize))
|
||||
if refreshChunks > 1 {
|
||||
count += refreshChunks
|
||||
// Add chunks for access token if needed
|
||||
accessChunks := calculateChunks(accessToken)
|
||||
if accessChunks > 0 {
|
||||
count += accessChunks
|
||||
}
|
||||
|
||||
// Add chunks for refresh token if needed
|
||||
refreshChunks := calculateChunks(refreshToken)
|
||||
if refreshChunks > 0 {
|
||||
count += refreshChunks
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user