mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
1b49e133da
* Fix bug affecting Azure OIDC authentication ( and most likely others ) * Fixes issue #51 * Ensure that appended roles are unique. Update the documentation. * Improvements targetting possible memory usage spikes. * Additional fixes and cleanup * Refactoring code to fix the issues identified by the users. * Modernize run * Fieldalignment * Multiple changes to improve performance and reduce complexity. - Optimise the errors and recovery. - Deduplicate code in metadata cache. - Remove unused performance monitoring code. - Simplify session management and settings handling. * Fix claims issue. * Add ability to overwrite the default scopes in the settings file * Well.. that escalated quickly. Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ). * Bugfix #51: Ensures that user provided scopes overrides work. * fixup! Bugfix #51: Ensures that user provided scopes overrides work. * fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work. * Abstract the provider logic into a separate package. * Additional micro fixes and cleanups. * Simplify all the things. * fixup! Simplify all the things. * fixup! fixup! Simplify all the things. * fixup! fixup! fixup! Simplify all the things. * fixup! fixup! fixup! fixup! Simplify all the things. * ... * Cleanup tests. * fixup! Cleanup tests. * fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! fixup! Cleanup tests. * Issue #53: Fix CSRF token handling in reverse proxy 1. ✅ HTTPS Detection Fixed (session.go:723) - Now uses X-Forwarded-Proto header instead of r.URL.Scheme - Properly detects HTTPS in reverse proxy environments 2. ✅ SameSite Cookie Attribute Fixed - Removed automatic SameSiteStrictMode for HTTPS (would break OAuth) - Keeps SameSiteLaxMode to allow OAuth callbacks from external domains - Only uses Strict for AJAX requests which don't involve OAuth redirects 3. ✅ Cookie Domain Handling Fixed - Now respects X-Forwarded-Host header for cookie domain - Ensures cookies are set for the public domain, not internal proxy domain 4. ✅ EnhanceSessionSecurity Properly Integrated - Function is now actually called during session save - Applies security enhancements without breaking OAuth flow Why Issue #53 Failed Before: 1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back) 2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail 3. Cookie domain might have been wrong (internal vs public domain) Why It Works Now: 1. Cookies are properly marked Secure for HTTPS 2. Uses SameSite=Lax to allow OAuth provider callbacks 3. Cookie domain uses public domain from X-Forwarded-Host 4. CSRF token persists through the entire OAuth flow * Next set of enhancements together with memory usage improvements. * Memory leak fixes and optimisations. * CSRF and Cookie Domain fixes * fixup! CSRF and Cookie Domain fixes * Metadata cache leak fix + profiling * fixup! Metadata cache leak fix + profiling * Memory leaks hunting, part 1337. * Further pursue of perfection. * fixup! Further pursue of perfection. * fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * Clear race conditions * fixup! Clear race conditions * Weekend fun with memory leaks * Splitting code into multiple files with reasonable testing coverage. ``` ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements ```` * fixup! Splitting code into multiple files with reasonable testing coverage. * fixup! fixup! Splitting code into multiple files with reasonable testing coverage. * Weekend fun with further optimisations. * fixup! Weekend fun with further optimisations. * fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * Pre-release cleanup. * Enhance test coverage. * fixup! Enhance test coverage. * fixup! fixup! Enhance test coverage. * fixup! fixup! fixup! Enhance test coverage.
330 lines
9.0 KiB
Go
330 lines
9.0 KiB
Go
// Package storage provides session storage operations for the OIDC middleware
|
|
package storage
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/gorilla/sessions"
|
|
)
|
|
|
|
// SessionData represents a user's authentication session with comprehensive token management.
|
|
// It handles main session data and supports large tokens that need to be
|
|
// split across multiple cookies due to browser size limitations.
|
|
type SessionData struct {
|
|
manager SessionManager
|
|
request *http.Request
|
|
mainSession *sessions.Session
|
|
accessSession *sessions.Session
|
|
refreshSession *sessions.Session
|
|
idTokenSession *sessions.Session
|
|
accessTokenChunks map[int]*sessions.Session
|
|
refreshTokenChunks map[int]*sessions.Session
|
|
idTokenChunks map[int]*sessions.Session
|
|
refreshMutex sync.Mutex
|
|
sessionMutex sync.RWMutex
|
|
dirty bool
|
|
inUse bool
|
|
}
|
|
|
|
// ChunkCleaner interface for chunk cleanup operations
|
|
type ChunkCleaner interface {
|
|
CleanupChunks(chunks map[int]*sessions.Session, w http.ResponseWriter)
|
|
}
|
|
|
|
// SessionManager interface for session management operations
|
|
type SessionManager interface {
|
|
GetSessionOptions(isSecure bool) *sessions.Options
|
|
EnhanceSessionSecurity(options *sessions.Options, r *http.Request) *sessions.Options
|
|
GetLogger() Logger
|
|
}
|
|
|
|
// Logger interface for dependency injection
|
|
type Logger interface {
|
|
Error(msg string)
|
|
Errorf(format string, args ...interface{})
|
|
}
|
|
|
|
// NewSessionData creates a new session data instance
|
|
func NewSessionData(manager SessionManager) *SessionData {
|
|
return &SessionData{
|
|
manager: manager,
|
|
accessTokenChunks: make(map[int]*sessions.Session),
|
|
refreshTokenChunks: make(map[int]*sessions.Session),
|
|
idTokenChunks: make(map[int]*sessions.Session),
|
|
refreshMutex: sync.Mutex{},
|
|
sessionMutex: sync.RWMutex{},
|
|
dirty: false,
|
|
inUse: false,
|
|
}
|
|
}
|
|
|
|
// IsDirty returns true if the session data has been modified since it was last loaded or saved.
|
|
// This is used to optimize session saves by only writing when necessary.
|
|
func (sd *SessionData) IsDirty() bool {
|
|
sd.sessionMutex.RLock()
|
|
defer sd.sessionMutex.RUnlock()
|
|
return sd.dirty
|
|
}
|
|
|
|
// MarkDirty marks the session as having pending changes that need to be saved.
|
|
// This is used when session data hasn't changed in content but should still
|
|
// trigger a session save (e.g., to ensure the cookie is re-issued).
|
|
func (sd *SessionData) MarkDirty() {
|
|
sd.sessionMutex.Lock()
|
|
defer sd.sessionMutex.Unlock()
|
|
sd.dirty = true
|
|
}
|
|
|
|
// Save persists all session data including main session and token chunks.
|
|
// It applies security options, saves all session components, and handles
|
|
// errors gracefully by continuing to save other components even if one fails.
|
|
func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error {
|
|
isSecure := r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil
|
|
if forceHTTPS := sd.manager.GetLogger(); forceHTTPS != nil {
|
|
// Add force HTTPS check if needed
|
|
}
|
|
|
|
options := sd.manager.GetSessionOptions(isSecure)
|
|
options = sd.manager.EnhanceSessionSecurity(options, r)
|
|
|
|
if sd.mainSession != nil {
|
|
sd.mainSession.Options = options
|
|
}
|
|
if sd.accessSession != nil {
|
|
sd.accessSession.Options = options
|
|
}
|
|
if sd.refreshSession != nil {
|
|
sd.refreshSession.Options = options
|
|
}
|
|
if sd.idTokenSession != nil {
|
|
sd.idTokenSession.Options = options
|
|
}
|
|
|
|
var firstErr error
|
|
saveOrLogError := func(s *sessions.Session, name string) {
|
|
if s == nil {
|
|
logger := sd.manager.GetLogger()
|
|
if logger != nil {
|
|
logger.Errorf("Attempted to save nil session: %s", name)
|
|
}
|
|
if firstErr == nil {
|
|
firstErr = fmt.Errorf("attempted to save nil session: %s", name)
|
|
}
|
|
return
|
|
}
|
|
if err := s.Save(r, w); err != nil {
|
|
errMsg := fmt.Errorf("failed to save %s session: %w", name, err)
|
|
logger := sd.manager.GetLogger()
|
|
if logger != nil {
|
|
logger.Error(errMsg.Error())
|
|
}
|
|
if firstErr == nil {
|
|
firstErr = errMsg
|
|
}
|
|
}
|
|
}
|
|
|
|
saveOrLogError(sd.mainSession, "main")
|
|
saveOrLogError(sd.accessSession, "access token")
|
|
saveOrLogError(sd.refreshSession, "refresh token")
|
|
saveOrLogError(sd.idTokenSession, "ID token")
|
|
|
|
for i, sessionChunk := range sd.accessTokenChunks {
|
|
if sessionChunk != nil {
|
|
sessionChunk.Options = options
|
|
saveOrLogError(sessionChunk, fmt.Sprintf("access token chunk %d", i))
|
|
}
|
|
}
|
|
|
|
for i, sessionChunk := range sd.refreshTokenChunks {
|
|
if sessionChunk != nil {
|
|
sessionChunk.Options = options
|
|
saveOrLogError(sessionChunk, fmt.Sprintf("refresh token chunk %d", i))
|
|
}
|
|
}
|
|
|
|
for i, sessionChunk := range sd.idTokenChunks {
|
|
if sessionChunk != nil {
|
|
sessionChunk.Options = options
|
|
saveOrLogError(sessionChunk, fmt.Sprintf("ID token chunk %d", i))
|
|
}
|
|
}
|
|
|
|
if firstErr == nil {
|
|
sd.dirty = false
|
|
}
|
|
return firstErr
|
|
}
|
|
|
|
// Clear completely clears all session data and safely returns the session to the pool.
|
|
// It removes all authentication data, expires cookies, and handles panic recovery.
|
|
func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
|
defer func() {
|
|
sd.returnToPoolSafely()
|
|
}()
|
|
|
|
sd.sessionMutex.Lock()
|
|
defer sd.sessionMutex.Unlock()
|
|
|
|
sd.clearAllSessionData(r, true)
|
|
|
|
// This is primarily for testing - in production w will often be nil
|
|
var err error
|
|
if w != nil {
|
|
if r != nil && r.Header.Get("X-Test-Error") == "true" {
|
|
if sd.mainSession != nil {
|
|
sd.mainSession.Values["error_trigger"] = func() {}
|
|
}
|
|
}
|
|
|
|
err = sd.Save(r, w)
|
|
}
|
|
|
|
sd.request = nil
|
|
return err
|
|
}
|
|
|
|
// clearAllSessionData clears all session data including main session and token chunks.
|
|
// It removes all session values and optionally expires all associated cookies.
|
|
func (sd *SessionData) clearAllSessionData(r *http.Request, expire bool) {
|
|
clearSessionValues(sd.mainSession, expire)
|
|
clearSessionValues(sd.accessSession, expire)
|
|
clearSessionValues(sd.refreshSession, expire)
|
|
clearSessionValues(sd.idTokenSession, expire)
|
|
|
|
if expire && r != nil {
|
|
sd.clearTokenChunks(r, sd.accessTokenChunks)
|
|
sd.clearTokenChunks(r, sd.refreshTokenChunks)
|
|
sd.clearTokenChunks(r, sd.idTokenChunks)
|
|
} else {
|
|
for k := range sd.accessTokenChunks {
|
|
delete(sd.accessTokenChunks, k)
|
|
}
|
|
for k := range sd.refreshTokenChunks {
|
|
delete(sd.refreshTokenChunks, k)
|
|
}
|
|
for k := range sd.idTokenChunks {
|
|
delete(sd.idTokenChunks, k)
|
|
}
|
|
}
|
|
|
|
if expire {
|
|
sd.dirty = true
|
|
}
|
|
}
|
|
|
|
// clearSessionValues removes all values from a session and optionally expires it.
|
|
// This is used during session cleanup and logout operations.
|
|
func clearSessionValues(session *sessions.Session, expire bool) {
|
|
if session == nil {
|
|
return
|
|
}
|
|
|
|
for k := range session.Values {
|
|
delete(session.Values, k)
|
|
}
|
|
|
|
if expire {
|
|
session.Options.MaxAge = -1
|
|
}
|
|
}
|
|
|
|
// clearTokenChunks clears token chunks from the session
|
|
func (sd *SessionData) clearTokenChunks(r *http.Request, chunks map[int]*sessions.Session) {
|
|
for i, chunk := range chunks {
|
|
if chunk != nil {
|
|
clearSessionValues(chunk, true)
|
|
}
|
|
delete(chunks, i)
|
|
}
|
|
}
|
|
|
|
// returnToPoolSafely safely returns the session to the object pool
|
|
func (sd *SessionData) returnToPoolSafely() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logger := sd.manager.GetLogger()
|
|
if logger != nil {
|
|
logger.Errorf("Panic during session pool return: %v", r)
|
|
}
|
|
}
|
|
}()
|
|
|
|
sd.sessionMutex.Lock()
|
|
defer sd.sessionMutex.Unlock()
|
|
|
|
if sd.inUse {
|
|
sd.inUse = false
|
|
sd.Reset()
|
|
// Pool return should be handled by calling code
|
|
}
|
|
}
|
|
|
|
// Reset resets the session data to a clean state
|
|
func (sd *SessionData) Reset() {
|
|
sd.mainSession = nil
|
|
sd.accessSession = nil
|
|
sd.refreshSession = nil
|
|
sd.idTokenSession = nil
|
|
|
|
// Clear maps without recreating them
|
|
for k := range sd.accessTokenChunks {
|
|
delete(sd.accessTokenChunks, k)
|
|
}
|
|
for k := range sd.refreshTokenChunks {
|
|
delete(sd.refreshTokenChunks, k)
|
|
}
|
|
for k := range sd.idTokenChunks {
|
|
delete(sd.idTokenChunks, k)
|
|
}
|
|
|
|
sd.dirty = false
|
|
sd.inUse = false
|
|
sd.request = nil
|
|
}
|
|
|
|
// SetSessions sets the session objects
|
|
func (sd *SessionData) SetSessions(main, access, refresh, idToken *sessions.Session) {
|
|
sd.mainSession = main
|
|
sd.accessSession = access
|
|
sd.refreshSession = refresh
|
|
sd.idTokenSession = idToken
|
|
}
|
|
|
|
// GetMainSession returns the main session
|
|
func (sd *SessionData) GetMainSession() *sessions.Session {
|
|
return sd.mainSession
|
|
}
|
|
|
|
// GetAccessSession returns the access token session
|
|
func (sd *SessionData) GetAccessSession() *sessions.Session {
|
|
return sd.accessSession
|
|
}
|
|
|
|
// GetRefreshSession returns the refresh token session
|
|
func (sd *SessionData) GetRefreshSession() *sessions.Session {
|
|
return sd.refreshSession
|
|
}
|
|
|
|
// GetIDTokenSession returns the ID token session
|
|
func (sd *SessionData) GetIDTokenSession() *sessions.Session {
|
|
return sd.idTokenSession
|
|
}
|
|
|
|
// GetTokenChunks returns the token chunk maps
|
|
func (sd *SessionData) GetTokenChunks() (map[int]*sessions.Session, map[int]*sessions.Session, map[int]*sessions.Session) {
|
|
return sd.accessTokenChunks, sd.refreshTokenChunks, sd.idTokenChunks
|
|
}
|
|
|
|
// SetInUse marks the session as in use
|
|
func (sd *SessionData) SetInUse(inUse bool) {
|
|
sd.inUse = inUse
|
|
}
|
|
|
|
// IsInUse returns whether the session is in use
|
|
func (sd *SessionData) IsInUse() bool {
|
|
return sd.inUse
|
|
}
|