mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
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
````
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user