// 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 } // 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 { 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.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 }