mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
796 lines
27 KiB
Go
796 lines
27 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/sessions"
|
|
)
|
|
|
|
// generateSecureRandomString creates a cryptographically secure, hex-encoded random string.
|
|
// It reads the specified number of bytes from crypto/rand and encodes them as a hexadecimal string.
|
|
//
|
|
// Parameters:
|
|
// - length: The number of random bytes to generate (the resulting hex string will be twice this length).
|
|
//
|
|
// Returns:
|
|
// - A hex-encoded random string.
|
|
// - An error if reading random bytes fails.
|
|
func generateSecureRandomString(length int) (string, error) {
|
|
bytes := make([]byte, length)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|
|
|
|
// Cookie names and configuration constants used for session management
|
|
const (
|
|
// Using fixed prefixes for consistent cookie naming across restarts
|
|
mainCookieName = "_oidc_raczylo_m"
|
|
accessTokenCookie = "_oidc_raczylo_a"
|
|
refreshTokenCookie = "_oidc_raczylo_r"
|
|
)
|
|
|
|
const (
|
|
// maxCookieSize is the maximum size for each cookie chunk.
|
|
// This value is calculated to ensure the final cookie size stays within browser limits:
|
|
// 1. Browser cookie size limit is typically 4096 bytes
|
|
// 2. Cookie content undergoes encryption (adds 28 bytes) and base64 encoding (4/3 ratio)
|
|
// 3. Calculation:
|
|
// - Let x be the chunk size
|
|
// - After encryption: x + 28 bytes
|
|
// - After base64: ((x + 28) * 4/3) bytes
|
|
// - Must satisfy: ((x + 28) * 4/3) ≤ 4096
|
|
// - Solving for x: x ≤ 3044
|
|
// 4. We use 2000 as a conservative limit to account for cookie metadata
|
|
maxCookieSize = 2000
|
|
|
|
// absoluteSessionTimeout defines the maximum lifetime of a session
|
|
// regardless of activity (24 hours)
|
|
absoluteSessionTimeout = 24 * time.Hour
|
|
|
|
// minEncryptionKeyLength defines the minimum length for the encryption key
|
|
minEncryptionKeyLength = 32
|
|
)
|
|
|
|
// compressToken compresses the input string using gzip and then encodes the result using standard base64 encoding.
|
|
// If any error occurs during compression, it returns the original uncompressed token as a fallback.
|
|
//
|
|
// Parameters:
|
|
// - token: The string to compress.
|
|
//
|
|
// Returns:
|
|
// - The base64 encoded, gzipped string, or the original string if compression fails.
|
|
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 decodes a standard base64 encoded string and then decompresses the result using gzip.
|
|
// If base64 decoding or gzip decompression fails, it returns the original input string as a fallback,
|
|
// assuming it might not have been compressed.
|
|
//
|
|
// Parameters:
|
|
// - compressed: The base64 encoded, gzipped string.
|
|
//
|
|
// Returns:
|
|
// - The decompressed original string, or the input string if decompression fails.
|
|
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.
|
|
type SessionManager struct {
|
|
// store is the underlying session store for cookie management.
|
|
store sessions.Store
|
|
|
|
// forceHTTPS enforces secure cookie attributes regardless of request scheme.
|
|
forceHTTPS bool
|
|
|
|
// 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.
|
|
// Parameters:
|
|
// - encryptionKey: Key used to encrypt session data (must be at least 32 bytes)
|
|
// - forceHTTPS: When true, forces secure cookie attributes regardless of request scheme
|
|
// - logger: Logger instance for recording session-related events
|
|
//
|
|
// Returns an error if the encryption key does not meet minimum length requirements.
|
|
func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*SessionManager, error) {
|
|
// Validate encryption key length.
|
|
if len(encryptionKey) < minEncryptionKeyLength {
|
|
return nil, fmt.Errorf("encryption key must be at least %d bytes long", minEncryptionKeyLength)
|
|
}
|
|
|
|
sm := &SessionManager{
|
|
store: sessions.NewCookieStore([]byte(encryptionKey)),
|
|
forceHTTPS: forceHTTPS,
|
|
logger: logger,
|
|
}
|
|
|
|
// Initialize session pool.
|
|
sm.sessionPool.New = func() interface{} {
|
|
// Initialize SessionData with necessary fields and the mutex.
|
|
return &SessionData{
|
|
manager: sm,
|
|
accessTokenChunks: make(map[int]*sessions.Session),
|
|
refreshTokenChunks: make(map[int]*sessions.Session),
|
|
refreshMutex: sync.Mutex{}, // Initialize the mutex
|
|
}
|
|
}
|
|
|
|
return sm, nil
|
|
}
|
|
|
|
// getSessionOptions returns a sessions.Options struct configured with security best practices.
|
|
// It sets HttpOnly to true, Secure based on the request scheme or forceHTTPS setting,
|
|
// SameSite to LaxMode, MaxAge to the absoluteSessionTimeout, and Path to "/".
|
|
//
|
|
// Parameters:
|
|
// - isSecure: A boolean indicating if the current request context is secure (HTTPS).
|
|
//
|
|
// Returns:
|
|
// - A pointer to a configured sessions.Options struct.
|
|
func (sm *SessionManager) getSessionOptions(isSecure bool) *sessions.Options {
|
|
return &sessions.Options{
|
|
HttpOnly: true,
|
|
Secure: isSecure || sm.forceHTTPS,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: int(absoluteSessionTimeout.Seconds()),
|
|
Path: "/",
|
|
}
|
|
}
|
|
|
|
// GetSession retrieves all session data for the current request.
|
|
// It loads the main session and token sessions, including any chunked token data,
|
|
// 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) {
|
|
// 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)
|
|
}
|
|
|
|
// Check for absolute session timeout.
|
|
if createdAt, ok := sessionData.mainSession.Values["created_at"].(int64); ok {
|
|
if time.Since(time.Unix(createdAt, 0)) > absoluteSessionTimeout {
|
|
sessionData.Clear(r, nil)
|
|
return nil, fmt.Errorf("session expired")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 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 token sessions.
|
|
sm.getTokenChunkSessions(r, accessTokenCookie, sessionData.accessTokenChunks)
|
|
sm.getTokenChunkSessions(r, refreshTokenCookie, sessionData.refreshTokenChunks)
|
|
|
|
return sessionData, nil
|
|
}
|
|
|
|
// getTokenChunkSessions retrieves all cookie chunks associated with a large token (access or refresh).
|
|
// It iteratively attempts to load cookies named "{baseName}_0", "{baseName}_1", etc., until
|
|
// a cookie is not found or returns an error. The loaded sessions are stored in the provided chunks map.
|
|
//
|
|
// Parameters:
|
|
// - r: The incoming HTTP request containing the cookies.
|
|
// - baseName: The base name of the cookie (e.g., accessTokenCookie).
|
|
// - chunks: The map (typically SessionData.accessTokenChunks or SessionData.refreshTokenChunks) to populate with the found session chunks.
|
|
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)
|
|
if err != nil || session.IsNew {
|
|
break
|
|
}
|
|
chunks[i] = session
|
|
}
|
|
}
|
|
|
|
// SessionData holds all session information for an authenticated user.
|
|
// It manages multiple session cookies to handle the main session state
|
|
// and potentially large access and refresh tokens that may need to be
|
|
// split across multiple cookies due to browser size limitations.
|
|
type SessionData struct {
|
|
// manager is the SessionManager that created this SessionData.
|
|
manager *SessionManager
|
|
|
|
// request is the current HTTP request associated with this session.
|
|
request *http.Request
|
|
|
|
// mainSession stores authentication state and basic user info.
|
|
mainSession *sessions.Session
|
|
|
|
// accessSession stores the primary access token cookie.
|
|
accessSession *sessions.Session
|
|
|
|
// refreshSession stores the primary refresh token cookie.
|
|
refreshSession *sessions.Session
|
|
|
|
// accessTokenChunks stores additional chunks of the access token
|
|
// when it exceeds the maximum cookie size.
|
|
accessTokenChunks map[int]*sessions.Session
|
|
|
|
// refreshTokenChunks stores additional chunks of the refresh token
|
|
// when it exceeds the maximum cookie size.
|
|
refreshTokenChunks map[int]*sessions.Session
|
|
|
|
// refreshMutex protects refresh token operations within this session instance.
|
|
refreshMutex sync.Mutex
|
|
}
|
|
|
|
// Save persists all parts of the session (main, access token, refresh token, and any chunks)
|
|
// back to the client as cookies in the HTTP response. It applies secure cookie options
|
|
// obtained via getSessionOptions based on the request's security context.
|
|
//
|
|
// Parameters:
|
|
// - r: The original HTTP request (used to determine security context for cookie options).
|
|
// - w: The HTTP response writer to which the Set-Cookie headers will be added.
|
|
//
|
|
// Returns:
|
|
// - An error if saving any of the session components fails.
|
|
func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error {
|
|
isSecure := strings.HasPrefix(r.URL.Scheme, "https") || sd.manager.forceHTTPS
|
|
|
|
// Set options for all sessions.
|
|
options := sd.manager.getSessionOptions(isSecure)
|
|
sd.mainSession.Options = options
|
|
sd.accessSession.Options = options
|
|
sd.refreshSession.Options = options
|
|
|
|
// Save main session.
|
|
if err := sd.mainSession.Save(r, w); err != nil {
|
|
return fmt.Errorf("failed to save main session: %w", err)
|
|
}
|
|
|
|
// Save access token session.
|
|
if err := sd.accessSession.Save(r, w); err != nil {
|
|
return fmt.Errorf("failed to save access token session: %w", err)
|
|
}
|
|
|
|
// Save refresh token session.
|
|
if err := sd.refreshSession.Save(r, w); err != nil {
|
|
return fmt.Errorf("failed to save refresh token session: %w", err)
|
|
}
|
|
|
|
// Save access token chunks.
|
|
for _, session := range sd.accessTokenChunks {
|
|
session.Options = options
|
|
if err := session.Save(r, w); err != nil {
|
|
return fmt.Errorf("failed to save access token chunk session: %w", err)
|
|
}
|
|
}
|
|
|
|
// Save refresh token chunks.
|
|
for _, session := range sd.refreshTokenChunks {
|
|
session.Options = options
|
|
if err := session.Save(r, w); err != nil {
|
|
return fmt.Errorf("failed to save refresh token chunk session: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Clear removes all session data associated with this SessionData instance.
|
|
// It clears the values map of the main, access, and refresh sessions, sets their MaxAge to -1
|
|
// to expire the cookies immediately, and clears any associated token chunk cookies.
|
|
// If a ResponseWriter is provided, it attempts to save the expired sessions to send the
|
|
// expiring Set-Cookie headers. Finally, it clears internal fields and returns the SessionData
|
|
// object to the pool.
|
|
//
|
|
// Parameters:
|
|
// - r: The HTTP request (required by the underlying session store).
|
|
// - w: The HTTP response writer (optional). If provided, expiring Set-Cookie headers will be sent.
|
|
//
|
|
// Returns:
|
|
// - An error if saving the expired sessions fails (only if w is not nil).
|
|
func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
|
// Clear and expire all sessions.
|
|
sd.mainSession.Options.MaxAge = -1
|
|
sd.accessSession.Options.MaxAge = -1
|
|
sd.refreshSession.Options.MaxAge = -1
|
|
|
|
for k := range sd.mainSession.Values {
|
|
delete(sd.mainSession.Values, k)
|
|
}
|
|
for k := range sd.accessSession.Values {
|
|
delete(sd.accessSession.Values, k)
|
|
}
|
|
for k := range sd.refreshSession.Values {
|
|
delete(sd.refreshSession.Values, k)
|
|
}
|
|
|
|
// Clear chunk sessions.
|
|
sd.clearTokenChunks(r, sd.accessTokenChunks)
|
|
sd.clearTokenChunks(r, sd.refreshTokenChunks)
|
|
|
|
var err error
|
|
if w != nil {
|
|
err = sd.Save(r, w)
|
|
}
|
|
|
|
// Clear transient per-request fields.
|
|
sd.request = nil
|
|
|
|
// Return session to pool.
|
|
sd.manager.sessionPool.Put(sd)
|
|
|
|
return err
|
|
}
|
|
|
|
// clearTokenChunks iterates through a map of session chunks, clears their values,
|
|
// and sets their MaxAge to -1 to expire them. This is used internally by Clear.
|
|
//
|
|
// Parameters:
|
|
// - r: The HTTP request (required by the underlying session store, though not directly used here).
|
|
// - chunks: The map of session chunks (e.g., sd.accessTokenChunks) to clear and expire.
|
|
func (sd *SessionData) clearTokenChunks(r *http.Request, chunks map[int]*sessions.Session) {
|
|
for _, session := range chunks {
|
|
session.Options.MaxAge = -1
|
|
for k := range session.Values {
|
|
delete(session.Values, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetAuthenticated checks if the session is marked as authenticated and has not exceeded
|
|
// the absolute session timeout.
|
|
//
|
|
// Returns:
|
|
// - true if the "authenticated" flag is set to true and the session creation time is within the allowed timeout.
|
|
// - false otherwise.
|
|
func (sd *SessionData) GetAuthenticated() bool {
|
|
auth, _ := sd.mainSession.Values["authenticated"].(bool)
|
|
if !auth {
|
|
return false
|
|
}
|
|
|
|
// Check session expiration.
|
|
createdAt, ok := sd.mainSession.Values["created_at"].(int64)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return time.Since(time.Unix(createdAt, 0)) <= absoluteSessionTimeout
|
|
}
|
|
|
|
// SetAuthenticated sets the authentication status of the session.
|
|
// If setting to true, it generates a new secure session ID for the main session
|
|
// to prevent session fixation attacks and records the current time as the creation time.
|
|
//
|
|
// Parameters:
|
|
// - value: The boolean authentication status (true for authenticated, false otherwise).
|
|
//
|
|
// Returns:
|
|
// - An error if generating a new session ID fails when setting value to true.
|
|
func (sd *SessionData) SetAuthenticated(value bool) error {
|
|
if value {
|
|
id, err := generateSecureRandomString(32)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate secure session id: %w", err)
|
|
}
|
|
sd.mainSession.ID = id
|
|
sd.mainSession.Values["created_at"] = time.Now().Unix()
|
|
}
|
|
sd.mainSession.Values["authenticated"] = value
|
|
return nil
|
|
}
|
|
|
|
// GetAccessToken retrieves the access token stored in the session.
|
|
// It handles reassembling the token from multiple cookie chunks if necessary
|
|
// and decompresses it if it was stored compressed.
|
|
//
|
|
// Returns:
|
|
// - The complete, decompressed access token string, or an empty string if not found.
|
|
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
|
|
}
|
|
|
|
// Reassemble token from chunks.
|
|
if len(sd.accessTokenChunks) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var chunks []string
|
|
for i := 0; ; i++ {
|
|
session, ok := sd.accessTokenChunks[i]
|
|
if !ok {
|
|
break
|
|
}
|
|
chunk, _ := session.Values["token_chunk"].(string)
|
|
chunks = append(chunks, chunk)
|
|
}
|
|
|
|
token = strings.Join(chunks, "")
|
|
compressed, _ := sd.accessSession.Values["compressed"].(bool)
|
|
if compressed {
|
|
return decompressToken(token)
|
|
}
|
|
return token
|
|
}
|
|
|
|
// SetAccessToken stores the provided access token in the session.
|
|
// It first expires any existing access token chunk cookies.
|
|
// It then compresses the token. If the compressed token fits within a single cookie (maxCookieSize),
|
|
// it's stored directly in the primary access token session. Otherwise, the compressed token
|
|
// is split into chunks, and each chunk is stored in a separate numbered cookie (_oidc_raczylo_a_0, _oidc_raczylo_a_1, etc.).
|
|
//
|
|
// Parameters:
|
|
// - token: The access token string to store.
|
|
func (sd *SessionData) SetAccessToken(token string) {
|
|
// Expire any existing chunk cookies first.
|
|
if sd.request != nil {
|
|
sd.expireAccessTokenChunks(nil) // Will be saved when Save() is called.
|
|
}
|
|
|
|
// Clear and prepare chunks map for new token.
|
|
sd.accessTokenChunks = make(map[int]*sessions.Session)
|
|
|
|
// Compress token.
|
|
compressed := compressToken(token)
|
|
|
|
if len(compressed) <= maxCookieSize {
|
|
sd.accessSession.Values["token"] = compressed
|
|
sd.accessSession.Values["compressed"] = true
|
|
} else {
|
|
// Split compressed token into chunks.
|
|
sd.accessSession.Values["token"] = ""
|
|
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)
|
|
session.Values["token_chunk"] = chunk
|
|
sd.accessTokenChunks[i] = session
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetRefreshToken retrieves the refresh token stored in the session.
|
|
// It handles reassembling the token from multiple cookie chunks if necessary
|
|
// and decompresses it if it was stored compressed.
|
|
//
|
|
// Returns:
|
|
// - The complete, decompressed refresh token string, or an empty string if not found.
|
|
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
|
|
}
|
|
|
|
// Reassemble token from chunks.
|
|
if len(sd.refreshTokenChunks) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var chunks []string
|
|
for i := 0; ; i++ {
|
|
session, ok := sd.refreshTokenChunks[i]
|
|
if !ok {
|
|
break
|
|
}
|
|
chunk, _ := session.Values["token_chunk"].(string)
|
|
chunks = append(chunks, chunk)
|
|
}
|
|
|
|
token = strings.Join(chunks, "")
|
|
compressed, _ := sd.refreshSession.Values["compressed"].(bool)
|
|
if compressed {
|
|
return decompressToken(token)
|
|
}
|
|
return token
|
|
}
|
|
|
|
// SetRefreshToken stores the provided refresh token in the session.
|
|
// It first expires any existing refresh token chunk cookies.
|
|
// It then compresses the token. If the compressed token fits within a single cookie (maxCookieSize),
|
|
// it's stored directly in the primary refresh token session. Otherwise, the compressed token
|
|
// is split into chunks, and each chunk is stored in a separate numbered cookie (_oidc_raczylo_r_0, _oidc_raczylo_r_1, etc.).
|
|
//
|
|
// Parameters:
|
|
// - token: The refresh token string to store.
|
|
func (sd *SessionData) SetRefreshToken(token string) {
|
|
// Expire any existing chunk cookies first.
|
|
if sd.request != nil {
|
|
sd.expireRefreshTokenChunks(nil) // Will be saved when Save() is called.
|
|
}
|
|
|
|
// Clear and prepare chunks map for new token.
|
|
sd.refreshTokenChunks = make(map[int]*sessions.Session)
|
|
|
|
// Compress token.
|
|
compressed := compressToken(token)
|
|
|
|
if len(compressed) <= maxCookieSize {
|
|
sd.refreshSession.Values["token"] = compressed
|
|
sd.refreshSession.Values["compressed"] = true
|
|
} else {
|
|
// Split compressed token into chunks.
|
|
sd.refreshSession.Values["token"] = ""
|
|
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)
|
|
session.Values["token_chunk"] = chunk
|
|
sd.refreshTokenChunks[i] = session
|
|
}
|
|
}
|
|
}
|
|
|
|
// expireAccessTokenChunks finds all existing access token chunk cookies (_oidc_raczylo_a_N)
|
|
// associated with the current request, clears their values, and sets their MaxAge to -1.
|
|
// If a ResponseWriter is provided, it attempts to save the expired chunk sessions to send
|
|
// the expiring Set-Cookie headers. This is used internally when setting a new access token.
|
|
//
|
|
// Parameters:
|
|
// - w: The HTTP response writer (optional). If provided, expiring Set-Cookie headers will be sent.
|
|
func (sd *SessionData) expireAccessTokenChunks(w http.ResponseWriter) {
|
|
for i := 0; ; i++ {
|
|
sessionName := fmt.Sprintf("%s_%d", accessTokenCookie, i)
|
|
session, err := sd.manager.store.Get(sd.request, sessionName)
|
|
if err != nil || session.IsNew {
|
|
break
|
|
}
|
|
session.Options.MaxAge = -1
|
|
session.Values = make(map[interface{}]interface{})
|
|
if w != nil {
|
|
if err := session.Save(sd.request, w); err != nil {
|
|
sd.manager.logger.Errorf("failed to save expired access token cookie: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// expireRefreshTokenChunks finds all existing refresh token chunk cookies (_oidc_raczylo_r_N)
|
|
// associated with the current request, clears their values, and sets their MaxAge to -1.
|
|
// If a ResponseWriter is provided, it attempts to save the expired chunk sessions to send
|
|
// the expiring Set-Cookie headers. This is used internally when setting a new refresh token.
|
|
//
|
|
// Parameters:
|
|
// - w: The HTTP response writer (optional). If provided, expiring Set-Cookie headers will be sent.
|
|
func (sd *SessionData) expireRefreshTokenChunks(w http.ResponseWriter) {
|
|
for i := 0; ; i++ {
|
|
sessionName := fmt.Sprintf("%s_%d", refreshTokenCookie, i)
|
|
session, err := sd.manager.store.Get(sd.request, sessionName)
|
|
if err != nil || session.IsNew {
|
|
break
|
|
}
|
|
session.Options.MaxAge = -1
|
|
session.Values = make(map[interface{}]interface{})
|
|
if w != nil {
|
|
if err := session.Save(sd.request, w); err != nil {
|
|
sd.manager.logger.Errorf("failed to save expired refresh token cookie: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// splitIntoChunks divides a string `s` into a slice of strings, where each element
|
|
// has a maximum length of `chunkSize`.
|
|
//
|
|
// Parameters:
|
|
// - s: The string to split.
|
|
// - chunkSize: The maximum size of each chunk.
|
|
//
|
|
// Returns:
|
|
// - A slice of strings representing the chunks.
|
|
func splitIntoChunks(s string, chunkSize int) []string {
|
|
var chunks []string
|
|
for len(s) > 0 {
|
|
if len(s) > chunkSize {
|
|
chunks = append(chunks, s[:chunkSize])
|
|
s = s[chunkSize:]
|
|
} else {
|
|
chunks = append(chunks, s)
|
|
break
|
|
}
|
|
}
|
|
return chunks
|
|
}
|
|
|
|
// GetCSRF retrieves the Cross-Site Request Forgery (CSRF) token stored in the main session.
|
|
//
|
|
// Returns:
|
|
// - The CSRF token string, or an empty string if not set.
|
|
func (sd *SessionData) GetCSRF() string {
|
|
csrf, _ := sd.mainSession.Values["csrf"].(string)
|
|
return csrf
|
|
}
|
|
|
|
// SetCSRF stores the provided CSRF token string in the main session.
|
|
// This token is typically generated at the start of the authentication flow.
|
|
//
|
|
// Parameters:
|
|
// - token: The CSRF token to store.
|
|
func (sd *SessionData) SetCSRF(token string) {
|
|
sd.mainSession.Values["csrf"] = token
|
|
}
|
|
|
|
// GetNonce retrieves the OIDC nonce value stored in the main session.
|
|
// The nonce is used to associate an ID token with the specific authentication request.
|
|
//
|
|
// Returns:
|
|
// - The nonce string, or an empty string if not set.
|
|
func (sd *SessionData) GetNonce() string {
|
|
nonce, _ := sd.mainSession.Values["nonce"].(string)
|
|
return nonce
|
|
}
|
|
|
|
// SetNonce stores the provided OIDC nonce string in the main session.
|
|
// This nonce is typically generated at the start of the authentication flow.
|
|
//
|
|
// Parameters:
|
|
// - nonce: The nonce string to store.
|
|
func (sd *SessionData) SetNonce(nonce string) {
|
|
sd.mainSession.Values["nonce"] = nonce
|
|
}
|
|
|
|
// GetCodeVerifier retrieves the PKCE (Proof Key for Code Exchange) code verifier
|
|
// stored in the main session. This is only relevant if PKCE is enabled.
|
|
//
|
|
// Returns:
|
|
// - The code verifier string, or an empty string if not set or PKCE is disabled.
|
|
func (sd *SessionData) GetCodeVerifier() string {
|
|
codeVerifier, _ := sd.mainSession.Values["code_verifier"].(string)
|
|
return codeVerifier
|
|
}
|
|
|
|
// SetCodeVerifier stores the provided PKCE code verifier string in the main session.
|
|
// This is typically called at the start of the authentication flow if PKCE is enabled.
|
|
//
|
|
// Parameters:
|
|
// - codeVerifier: The PKCE code verifier string to store.
|
|
func (sd *SessionData) SetCodeVerifier(codeVerifier string) {
|
|
sd.mainSession.Values["code_verifier"] = codeVerifier
|
|
}
|
|
|
|
// GetEmail retrieves the authenticated user's email address stored in the main session.
|
|
// This is typically extracted from the ID token claims after successful authentication.
|
|
//
|
|
// Returns:
|
|
// - The user's email address string, or an empty string if not set.
|
|
func (sd *SessionData) GetEmail() string {
|
|
email, _ := sd.mainSession.Values["email"].(string)
|
|
return email
|
|
}
|
|
|
|
// SetEmail stores the provided user email address string in the main session.
|
|
// This is typically called after successful authentication and claim extraction.
|
|
//
|
|
// Parameters:
|
|
// - email: The user's email address to store.
|
|
func (sd *SessionData) SetEmail(email string) {
|
|
sd.mainSession.Values["email"] = email
|
|
}
|
|
|
|
// GetIncomingPath retrieves the original request URI (including query parameters)
|
|
// that the user was trying to access before being redirected for authentication.
|
|
// This is stored in the main session to allow redirection back after successful login.
|
|
//
|
|
// Returns:
|
|
// - The original request URI string, or an empty string if not set.
|
|
func (sd *SessionData) GetIncomingPath() string {
|
|
path, _ := sd.mainSession.Values["incoming_path"].(string)
|
|
return path
|
|
}
|
|
|
|
// SetIncomingPath stores the original request URI (path and query parameters)
|
|
// in the main session. This is typically called at the start of the authentication flow.
|
|
//
|
|
// Parameters:
|
|
// - path: The original request URI string (e.g., "/protected/resource?id=123").
|
|
func (sd *SessionData) SetIncomingPath(path string) {
|
|
sd.mainSession.Values["incoming_path"] = path
|
|
}
|
|
|
|
// GetIDToken retrieves the ID token stored in the session.
|
|
// It handles reassembling the token from multiple cookie chunks if necessary
|
|
// and decompresses it if it was stored compressed.
|
|
//
|
|
// Returns:
|
|
// - The complete, decompressed ID token string, or an empty string if not found.
|
|
func (sd *SessionData) GetIDToken() string {
|
|
token, _ := sd.mainSession.Values["id_token"].(string)
|
|
if token != "" {
|
|
compressed, _ := sd.mainSession.Values["id_token_compressed"].(bool)
|
|
if compressed {
|
|
return decompressToken(token)
|
|
}
|
|
return token
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// SetIDToken stores the provided ID token in the session.
|
|
//
|
|
// Parameters:
|
|
// - token: The ID token string to store.
|
|
func (sd *SessionData) SetIDToken(token string) {
|
|
if token == "" {
|
|
sd.mainSession.Values["id_token"] = ""
|
|
sd.mainSession.Values["id_token_compressed"] = false
|
|
return
|
|
}
|
|
|
|
// Compress token
|
|
compressed := compressToken(token)
|
|
|
|
sd.mainSession.Values["id_token"] = compressed
|
|
sd.mainSession.Values["id_token_compressed"] = true
|
|
}
|