mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
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
|
|
}
|