fiber/docs/middleware/session.md
Jason McNeil 979e7cd6b1
feat(middleware/session): Introduce Extractor pattern for session ID retrieval (#3625)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-07-28 16:48:22 +02:00

24 KiB

id
id
session

Session

The Session middleware provides robust session management for Fiber applications, utilizing the Storage package for multi-database support via a unified interface. By default, session data is stored in memory, but custom storage options are easily configurable.

Table of Contents

Quick Start

import (
    "fmt"
    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/session"
)

// Basic usage
app.Use(session.New())

app.Get("/", func(c fiber.Ctx) error {
    sess := session.FromContext(c)
    
    // Get and update visits count
    var visits int
    if v := sess.Get("visits"); v != nil {
        // Use type assertion with an ok check to prevent a panic
        if vInt, ok := v.(int); ok {
            visits = vInt
        }
    }
    visits++
    sess.Set("visits", visits)
    return c.SendString(fmt.Sprintf("Visits: %d", visits))
})

Production Configuration

import (
    "time"
    "github.com/gofiber/storage/redis"
)

storage := redis.New(redis.Config{
    Host: "localhost",
    Port: 6379,
})

app.Use(session.New(session.Config{
    Storage:           storage,
    CookieSecure:      true,              // HTTPS only
    CookieHTTPOnly:    true,              // Prevent XSS
    CookieSameSite:    "Lax",             // CSRF protection
    IdleTimeout:       30 * time.Minute,  // Session timeout
    AbsoluteTimeout:   24 * time.Hour,    // Maximum session life
    Extractor:         session.FromCookie("__Host-session_id"),
}))

Usage Patterns

The middleware pattern automatically manages session lifecycle and is the recommended approach for most applications.

// Setup middleware
app.Use(session.New())

// Use in handlers
app.Post("/login", func(c fiber.Ctx) error {
    sess := session.FromContext(c)
    
    // Session is automatically saved when handler returns
    sess.Set("user_id", 123)
    sess.Set("authenticated", true)
    
    return c.Redirect("/dashboard")
})

Benefits:

  • Automatic session saving
  • Automatic resource cleanup
  • No manual lifecycle management
  • Thread-safe operations

Store Pattern (Advanced)

Use the store pattern for background tasks or when you need direct session access.

import (
    "context"
    "log"
    "time"
)

store := session.NewStore()

// In background tasks
func backgroundTask(sessionID string) {
    sess, err := store.GetByID(context.Background(), sessionID)
    if err != nil {
        return
    }
    defer sess.Release() // Important: Manual cleanup required
    
    // Modify session
    sess.Set("last_task", time.Now())
    
    // Manual save required
    if err := sess.Save(); err != nil {
        log.Printf("Failed to save session: %v", err)
    }
}

Requirements:

  • Must call sess.Release() when done
  • Must call sess.Save() to persist changes
  • Handle errors manually

Session Security

Authentication Flow

Understanding session lifecycle during authentication is crucial for security.

Basic Login/Logout

app.Post("/login", func(c fiber.Ctx) error {
    sess := session.FromContext(c)
    
    email := c.FormValue("email")
    password := c.FormValue("password")
    
    // Simple credential validation (use proper authentication in production)
    if email == "admin@example.com" && password == "secret" {
        // CRITICAL: Regenerate session ID to prevent session fixation
        // This changes the session ID while preserving existing data
        if err := sess.Regenerate(); err != nil {
            return c.Status(500).SendString("Session error")
        }
        
        // Add authentication data to existing session
        sess.Set("user_id", 1)
        sess.Set("authenticated", true)
        
        return c.Redirect("/dashboard")
    }
    
    return c.Status(401).SendString("Invalid credentials")
})

app.Post("/logout", func(c fiber.Ctx) error {
    sess := session.FromContext(c)
    
    // Complete session reset (clears all data + new session ID)
    if err := sess.Reset(); err != nil {
        return c.Status(500).SendString("Session error")
    }
    
    return c.Redirect("/")
})

Cart Preservation During Login

app.Post("/login", func(c fiber.Ctx) error {
    sess := session.FromContext(c)
    
    // Validate credentials (implement your own validation)
    email := c.FormValue("email")
    password := c.FormValue("password")
    if !isValidUser(email, password) {
        return c.Status(401).JSON(fiber.Map{"error": "Invalid credentials"})
    }
    
    // CRITICAL: Regenerate session ID to prevent session fixation
    // This changes the session ID while preserving existing data
    if err := sess.Regenerate(); err != nil {
        return c.Status(500).JSON(fiber.Map{"error": "Session error"})
    }
    
    // Add authentication data to existing session
    sess.Set("user_id", getUserID(email))
    sess.Set("authenticated", true)
    sess.Set("login_time", time.Now())
    
    return c.JSON(fiber.Map{"status": "logged in"})
})

Security Methods Comparison

Method Session ID Session Data Use Case
Regenerate() Changes Preserved Login, privilege escalation
Reset() Changes Cleared Logout, security breach
Destroy() Unchanged Cleared Clear data only

Common Security Mistakes

Session Fixation Vulnerability:

// DANGEROUS: Keeping same session ID after login
app.Post("/login", func(c fiber.Ctx) error {
    sess := session.FromContext(c)
    // Validate user...
    sess.Set("user_id", userID) // Attacker can hijack this session!
    return c.Redirect("/dashboard")
})

Secure Implementation:

// SECURE: Always regenerate session ID after authentication
app.Post("/login", func(c fiber.Ctx) error {
    sess := session.FromContext(c)
    // Validate user...
    if err := sess.Regenerate(); err != nil { // Prevents session fixation
        return err
    }
    sess.Set("user_id", userID)
    return c.Redirect("/dashboard")
})

Authentication Middleware

This is a basic example of an authentication middleware that checks if a user is logged in before accessing protected routes.

// Authentication check middleware
func RequireAuth(c fiber.Ctx) error {
    sess := session.FromContext(c)
    if sess == nil {
        return c.Redirect("/login")
    }
    
    // Check if user is authenticated
    if sess.Get("authenticated") != true {
        return c.Redirect("/login")
    }
    
    return c.Next()
}

// Usage
app.Use("/dashboard", RequireAuth)
app.Use("/admin", RequireAuth)

Automatic Session Expiration

Sessions automatically expire based on your configuration:

app.Use(session.New(session.Config{
    IdleTimeout:     30 * time.Minute, // Auto-expire after 30 min of inactivity
    AbsoluteTimeout: 24 * time.Hour,   // Force expire after 24 hours regardless of activity
}))

How it works:

  • IdleTimeout: Storage automatically removes sessions after inactivity period
    • Any route that uses the middleware will reset the idle timer
    • Calling sess.Save() will also reset the idle timer
  • AbsoluteTimeout: Sessions are forcibly expired after maximum duration
  • No manual cleanup required - the storage layer handles this

Session ID Extractors

Built-in Extractors

// Cookie-based (recommended for web apps)
session.FromCookie("session_id")

// Header-based (recommended for APIs)  
session.FromHeader("X-Session-ID")

// Form data
session.FromForm("session_id")

// URL query parameter
session.FromQuery("session_id")

// URL path parameter
session.FromParam("id")

Response Behavior with Extractors:

  • Cookie extractors: Set cookie in response
  • Header extractors: Set header in response
  • Query/Form/Param extractors: Read-only, do not set response values

Multiple Sources with Fallback

app.Use(session.New(session.Config{
    Extractor: session.Chain(
        session.FromCookie("session_id"),    // Try cookie first
        session.FromHeader("X-Session-ID"),  // Then header
        session.FromQuery("session_id"),     // Finally query
    ),
}))

Response Behavior with Chained Extractors:

The session middleware intelligently sets response values based on the extractors in your chain:

  • Cookie + Header extractors: Both cookie and header are set in the response
  • Only Cookie extractors: Only cookie is set in the response
  • Only Header extractors: Only header is set in the response
  • Only Query/Form/Param extractors: No response values are set (read-only)
  • Mixed extractors: Only cookie and header extractors set response values
// This will set both cookie and header in response
session.Chain(
    session.FromCookie("session_id"), 
    session.FromHeader("X-Session-ID")
)

// This will set only cookie in response
session.Chain(
    session.FromCookie("session_id"), 
    session.FromQuery("session_id")   // Ignored for response
)

// This will set nothing in response (read-only mode)
session.Chain(
    session.FromQuery("session_id"), 
    session.FromForm("session_id")
)

Custom Extractor

You can create custom extractors by returning a session.Extractor struct that defines how to extract the session ID from the request and how the middleware should handle responses.

The Source field is crucial as it controls whether the middleware sets response values:

  • SourceCookie: Sets cookies in the response
  • SourceHeader: Sets headers in the response
  • SourceOther: Read-only, no response values set
// Custom extractor for Authorization Bearer tokens
func FromAuthorization() session.Extractor {
    return session.Extractor{
        Extract: func(c fiber.Ctx) (string, error) {
            auth := c.Get("Authorization")
            if strings.HasPrefix(auth, "Bearer ") {
                sessionID := strings.TrimPrefix(auth, "Bearer ")
                if sessionID != "" {
                    return sessionID, nil
                }
            }
            return "", session.ErrMissingSessionIDInHeader
        },
        Source: session.SourceHeader, // This will set response headers
        Key:    "Authorization",
    }
}

app.Use(session.New(session.Config{
    Extractor: FromAuthorization(), // Will set Authorization header in response
}))
// Custom read-only extractor (no response setting)
func FromCustomParam() session.Extractor {
    return session.Extractor{
        Extract: func(c fiber.Ctx) (string, error) {
            sessionID := c.Get("X-Custom-Session")
            if sessionID == "" {
                return "", session.ErrMissingSessionIDInHeader
            }
            return sessionID, nil
        },
        Source: session.SourceOther, // Read-only, won't set responses
        Key:    "X-Custom-Session",
    }
}

app.Use(session.New(session.Config{
    Extractor: FromCustomParam(), // Will not set any response values
}))

Configuration

Storage Options

import (
    "github.com/gofiber/storage/redis"
    "github.com/gofiber/storage/postgres"
)

// Redis (recommended for production)
redisStorage := redis.New(redis.Config{
    Host:     "localhost",
    Port:     6379,
    Password: "",
    Database: 0,
})

// PostgreSQL
pgStorage := postgres.New(postgres.Config{
    Host:     "localhost",
    Port:     5432,
    Database: "sessions",
    Username: "user",
    Password: "pass",
})

app.Use(session.New(session.Config{
    Storage: redisStorage,
}))

Production Security Settings

import (
    "log"
    "time"
    "github.com/gofiber/utils/v2"
)

app.Use(session.New(session.Config{
    // Storage
    Storage: redisStorage,
    
    // Security
    CookieSecure:      true,    // HTTPS only (required in production)
    CookieHTTPOnly:    true,    // No JavaScript access (prevents XSS)
    CookieSameSite:    "Lax",   // CSRF protection
    
    // Session Management
    IdleTimeout:       30 * time.Minute,  // Inactivity timeout
    AbsoluteTimeout:   24 * time.Hour,    // Maximum session duration
    
    // Cookie Settings
    CookiePath:        "/",
    CookieDomain:      "example.com",
    CookieSessionOnly: false,   // Persist across browser restarts
    
    // Session ID
    Extractor:         session.FromCookie("__Host-session_id"),
    KeyGenerator:      utils.UUIDv4,
    
    // Error Handling
    ErrorHandler: func(c fiber.Ctx, err error) {
        log.Printf("Session error: %v", err)
    },
}))

Custom Types

Session data supports basic Go types by default:

  • string, int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • bool, float32, float64
  • []byte, complex64, complex128
  • interface{}

For custom types (structs, maps, slices), you must register them for encoding/decoding:

import "fmt"

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Role string `json:"role"`
}

// Method 1: Using NewWithStore
func main() {
    app := fiber.New()
    
    sessionMiddleware, store := session.NewWithStore()
    store.RegisterType(User{}) // Register custom type
    
    app.Use(sessionMiddleware)
    
    app.Get("/", func(c fiber.Ctx) error {
        sess := session.FromContext(c)
        
        // Use custom type
        sess.Set("user", User{ID: 123, Name: "John", Role: "admin"})
        
        user, ok := sess.Get("user").(User)
        if ok {
            return c.JSON(fiber.Map{"user": user.Name, "role": user.Role})
        }
        return c.SendString("No user found")
    })
    
    app.Listen(":3000")
}
// Method 2: Using separate store
store := session.NewStore()
store.RegisterType(User{})

app.Use(session.New(session.Config{
    Store: store,
}))

// Usage in handlers
sess.Set("user", User{ID: 123, Name: "John", Role: "admin"})
user, ok := sess.Get("user").(User)
if ok {
    fmt.Printf("User: %s (Role: %s)", user.Name, user.Role)
}

Important Notes:

  • Custom types must be registered before using them in sessions
  • Registration must happen during application startup
  • All instances of the application must register the same types
  • Types are encoded using Go's gob package

Migration Guide

v2 to v3 Breaking Changes

  1. Function Signature: session.New() now returns middleware handler, not store
  2. Session ID Extraction: KeyLookup replaced with Extractor functions
  3. Lifecycle Management: Manual Release() required for store pattern
  4. Timeout Handling: Expiration split into IdleTimeout and AbsoluteTimeout

Migration Examples

v2 Code:

store := session.New(session.Config{
    KeyLookup: "cookie:session_id",
})

app.Get("/", func(c fiber.Ctx) error {
    sess, err := store.Get(c)
    if err != nil {
        return err
    }
    // Session automatically saved and released
    sess.Set("key", "value")
    return nil
})

v3 Middleware Pattern (Recommended):

app.Use(session.New(session.Config{
    Extractor: session.FromCookie("session_id"),
}))

app.Get("/", func(c fiber.Ctx) error {
    sess := session.FromContext(c)
    // Session automatically saved and released
    sess.Set("key", "value")
    return nil
})

v3 Store Pattern (Advanced):

store := session.NewStore(session.Config{
    Extractor: session.FromCookie("session_id"),
})

app.Get("/", func(c fiber.Ctx) error {
    sess, err := store.Get(c)
    if err != nil {
        return err
    }
    defer sess.Release() // Manual cleanup required
    
    sess.Set("key", "value")
    return sess.Save() // Manual save required
})

KeyLookup to Extractor Migration

v2 KeyLookup v3 Extractor
"cookie:session_id" session.FromCookie("session_id")
"header:X-Session-ID" session.FromHeader("X-Session-ID")
"query:session_id" session.FromQuery("session_id")
"form:session_id" session.FromForm("session_id")
"cookie:sid,header:X-Sid" session.Chain(session.FromCookie("sid"), session.FromHeader("X-Sid"))

API Reference

sess := session.FromContext(c)

// Data operations
sess.Get(key any) any
sess.Set(key, value any)
sess.Delete(key any)
sess.Keys() []any

// Session management
sess.ID() string
sess.Fresh() bool
sess.Regenerate() error  // Change ID, keep data
sess.Reset() error       // Change ID, clear data
sess.Destroy() error     // Keep ID, clear data

// Store access
sess.Store() *session.Store

Store Methods

store := session.NewStore()

// Store operations
store.Get(c fiber.Ctx) (*session.Session, error)
store.GetByID(ctx context.Context, sessionID string) (*session.Session, error)
store.Reset(c fiber.Ctx) error
store.Delete(sessionID string) error

// Type registration
store.RegisterType(interface{})

Session Methods (Store Pattern)

sess, err := store.Get(c)
defer sess.Release() // Required!

// Same methods as middleware, plus:
sess.Save() error              // Manual save required
sess.SetIdleTimeout(duration)  // Per-session timeout
sess.Release()                 // Manual cleanup required

Extractor Functions

// Built-in extractors
session.FromCookie(key string) session.Extractor
session.FromHeader(key string) session.Extractor
session.FromQuery(key string) session.Extractor
session.FromForm(key string) session.Extractor
session.FromParam(key string) session.Extractor

// Chaining
session.Chain(extractors ...session.Extractor) session.Extractor

Config Properties

Property Type Description Default
Storage fiber.Storage Session storage backend memory.New()
Extractor session.Extractor Session ID extraction FromCookie("session_id")
KeyGenerator func() string Session ID generator utils.UUIDv4
IdleTimeout time.Duration Inactivity timeout 30 * time.Minute
AbsoluteTimeout time.Duration Maximum session duration 0 (unlimited)
CookieSecure bool HTTPS only false
CookieHTTPOnly bool No JavaScript access false
CookieSameSite string SameSite attribute "Lax"
CookiePath string Cookie path ""
CookieDomain string Cookie domain ""
CookieSessionOnly bool Session cookie false
ErrorHandler func(fiber.Ctx, error) Error callback DefaultErrorHandler

Examples

E-commerce with Cart Persistence

import (
    "time"
    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/session"
    "github.com/gofiber/storage/redis"
)

func main() {
    app := fiber.New()
    
    // Session middleware
    app.Use(session.New(session.Config{
        Storage:           redis.New(),
        CookieSecure:      true,
        CookieHTTPOnly:    true,
        CookieSameSite:    "Lax",
        IdleTimeout:       30 * time.Minute,
        AbsoluteTimeout:   24 * time.Hour,
        Extractor:         session.FromCookie("__Host-cart_session"),
    }))
    
    // Add to cart (anonymous user)
    app.Post("/cart/add", func(c fiber.Ctx) error {
        sess := session.FromContext(c)
        
        cart, _ := sess.Get("cart").([]string)
        cart = append(cart, c.FormValue("item_id"))
        sess.Set("cart", cart)
        
        return c.JSON(fiber.Map{"items": len(cart)})
    })
    
    // Login (preserve session data)
    app.Post("/login", func(c fiber.Ctx) error {
        sess := session.FromContext(c)
        
        // Simple validation (implement proper authentication)
        email := c.FormValue("email")
        password := c.FormValue("password")
        if email != "user@example.com" || password != "password" {
            return c.Status(401).JSON(fiber.Map{"error": "Invalid credentials"})
        }
        
        // Regenerate session ID for security
        // This changes the session ID while preserving existing data
        if err := sess.Regenerate(); err != nil {
            return c.Status(500).JSON(fiber.Map{"error": "Session error"})
        }
        
        sess.Set("user_id", 1)
        sess.Set("authenticated", true)
        
        return c.JSON(fiber.Map{"status": "logged in"})
    })
    
    // Logout (clear everything)
    app.Post("/logout", func(c fiber.Ctx) error {
        sess := session.FromContext(c)
        
        // Reset clears all data and generates new session ID
        if err := sess.Reset(); err != nil {
            return c.Status(500).JSON(fiber.Map{"error": "Session error"})
        }
        
        return c.JSON(fiber.Map{"status": "logged out"})
    })
    
    app.Listen(":3000")
}

// Helper functions (implement these properly in production)
func isValidUser(email, password string) bool {
    return email == "user@example.com" && password == "password"
}

func getUserID(email string) int {
    return 1 // Return actual user ID from database
}

API with Header-based Sessions

import (
    "time"
    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/session"
    "github.com/gofiber/storage/redis"
)

func main() {
    app := fiber.New()
    
    // API session middleware with header extraction
    app.Use(session.New(session.Config{
        Storage:   redis.New(),
        Extractor: session.FromHeader("X-Session-Token"),
        IdleTimeout: time.Hour,
    }))
    
    // API endpoint
    app.Post("/api/data", func(c fiber.Ctx) error {
        sess := session.FromContext(c)
        
        // Track API usage
        count, _ := sess.Get("api_calls").(int)
        count++
        sess.Set("api_calls", count)
        sess.Set("last_call", time.Now())
        
        return c.JSON(fiber.Map{
            "data": "some data",
            "calls": count,
        })
    })
    
    app.Listen(":3000")
}

Multi-source Session ID Support

import (
    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/session"
)

func main() {
    app := fiber.New()
    
    // Support multiple sources with priority
    app.Use(session.New(session.Config{
        Extractor: session.Chain(
            session.FromCookie("session_id"),    // 1st: Cookie (web)
            session.FromHeader("X-Session-ID"),  // 2nd: Header (API)
            session.FromQuery("session_id"),     // 3rd: Query (fallback)
        ),
    }))
    
    app.Get("/", func(c fiber.Ctx) error {
        sess := session.FromContext(c)
        
        // Works with any of the above methods
        return c.JSON(fiber.Map{
            "session_id": sess.ID(),
            "source": "multi-source",
        })
    })
    
    app.Listen(":3000")
}