Session Management with Redis

Learning Objectives: By the end of this lesson, you will be able to:

  • Implement secure session management using Redis as a distributed session store
  • Design session architectures that scale across multiple servers and applications
  • Handle session security including CSRF protection and secure token generation
  • Build production-ready session middleware with proper timeout and cleanup handling
  • Implement advanced features like single sign-on (SSO) and cross-domain session sharing

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Have you ever wondered how websites “remember” you’re logged in across different pages and visits? Or how services like Google keep you signed in across Gmail, YouTube, and Google Drive? The secret lies in sophisticated session management systems, and Redis is the backbone powering seamless user experiences for millions of users worldwide!

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Session Fundamentals - Sessions, cookies, security considerations, and Redis advantages
  2. Redis Session Store - Implementing distributed session backend with Go
  3. Session Middleware - Building reusable authentication and authorization middleware
  4. Security Best Practices - CSRF protection, secure tokens, session hijacking prevention
  5. Advanced Patterns - Multi-device sessions, SSO, and cross-domain session sharing
  6. Production Considerations - Cleanup, monitoring, and scaling strategies

Success Criteria:

Build production-ready session management systems that scale across multiple servers while maintaining security, performance, and user experience.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

What are Sessions?

Sessions maintain user state across stateless HTTP requests. Unlike cookies (client-side), sessions store data server-side and use session IDs for identification.

Real-World Analogy: Sessions are like coat check tickets at a restaurant. You get a numbered ticket (session ID cookie), and the restaurant uses it to retrieve your specific coat (session data) from their storage room (Redis).

Why Redis for Sessions:

  • Distributed: Share sessions across multiple application servers
  • Fast: In-memory access provides microsecond response times
  • Automatic Cleanup: Built-in TTL handling prevents memory leaks
  • Scalable: Redis Cluster supports millions of concurrent sessions

Session vs Cookies vs JWT Comparison

Aspect Sessions Cookies JWT
Storage Server-side Client-side Client-side
Security High (server controlled) Medium (client exposed) Medium (stateless)
Scalability Redis enables distribution Limited by size Stateless scaling
Revocation Immediate Difficult Complex
Use Case Web apps Simple state APIs/microservices

4. Live Coding Demonstration (20-25 minutes)

Complete Production Session Manager

package main

import (
    "context"
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/redis/go-redis/v9"
)

// SessionData holds all session information
type SessionData struct {
    UserID       int                    `json:"user_id"`
    Username     string                 `json:"username"`
    Email        string                 `json:"email"`
    Role         string                 `json:"role"`
    LoginTime    time.Time              `json:"login_time"`
    LastActivity time.Time              `json:"last_activity"`
    CSRFToken    string                 `json:"csrf_token"`
    Permissions  []string               `json:"permissions"`
    Metadata     map[string]interface{} `json:"metadata"`
}

// SessionManager provides Redis-backed session management
type SessionManager struct {
    client *redis.Client
    ctx    context.Context
    config SessionConfig
}

type SessionConfig struct {
    TTL           time.Duration
    CookieName    string
    CookieDomain  string
    SecureCookies bool
    CSRFEnabled   bool
}

func NewSessionManager(client *redis.Client, config SessionConfig) *SessionManager {
    return &SessionManager{
        client: client,
        ctx:    context.Background(),
        config: config,
    }
}

// CreateSession creates a new session for a user
func (sm *SessionManager) CreateSession(userID int, username, email, role string, permissions []string) (string, *SessionData, error) {
    sessionID, err := sm.generateSecureID()
    if err != nil {
        return "", nil, fmt.Errorf("failed to generate session ID: %w", err)
    }

    csrfToken := ""
    if sm.config.CSRFEnabled {
        csrfToken, err = sm.generateSecureID()
        if err != nil {
            return "", nil, fmt.Errorf("failed to generate CSRF token: %w", err)
        }
    }

    now := time.Now()
    sessionData := &SessionData{
        UserID:       userID,
        Username:     username,
        Email:        email,
        Role:         role,
        LoginTime:    now,
        LastActivity: now,
        CSRFToken:    csrfToken,
        Permissions:  permissions,
        Metadata:     make(map[string]interface{}),
    }

    if err := sm.storeSession(sessionID, sessionData); err != nil {
        return "", nil, err
    }

    log.Printf("Created session %s for user %s", sessionID[:8]+"...", username)
    return sessionID, sessionData, nil
}

func (sm *SessionManager) GetSession(sessionID string) (*SessionData, error) {
    if sessionID == "" {
        return nil, fmt.Errorf("empty session ID")
    }

    key := sm.sessionKey(sessionID)
    data, err := sm.client.Get(sm.ctx, key).Bytes()
    if err == redis.Nil {
        return nil, fmt.Errorf("session not found")
    } else if err != nil {
        return nil, fmt.Errorf("failed to retrieve session: %w", err)
    }

    var session SessionData
    if err := json.Unmarshal(data, &session); err != nil {
        return nil, fmt.Errorf("failed to unmarshal session: %w", err)
    }

    return &session, nil
}

func (sm *SessionManager) UpdateSession(sessionID string, session *SessionData) error {
    session.LastActivity = time.Now()
    return sm.storeSession(sessionID, session)
}

func (sm *SessionManager) DeleteSession(sessionID string) error {
    key := sm.sessionKey(sessionID)
    return sm.client.Del(sm.ctx, key).Err()
}

// Helper methods
func (sm *SessionManager) generateSecureID() (string, error) {
    bytes := make([]byte, 32)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(bytes), nil
}

func (sm *SessionManager) sessionKey(sessionID string) string {
    return fmt.Sprintf("session:%s", sessionID)
}

func (sm *SessionManager) storeSession(sessionID string, session *SessionData) error {
    data, err := json.Marshal(session)
    if err != nil {
        return fmt.Errorf("failed to marshal session: %w", err)
    }

    key := sm.sessionKey(sessionID)
    return sm.client.Set(sm.ctx, key, data, sm.config.TTL).Err()
}

// Middleware implementations
func (sm *SessionManager) SessionMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        cookie, err := c.Request.Cookie(sm.config.CookieName)
        if err != nil {
            c.Set("session", (*SessionData)(nil))
            c.Next()
            return
        }

        session, err := sm.GetSession(cookie.Value)
        if err != nil {
            c.Set("session", (*SessionData)(nil))
            c.Next()
            return
        }

        // Update last activity
        if err := sm.UpdateSession(cookie.Value, session); err != nil {
            log.Printf("Failed to update session activity: %v", err)
        }

        // Store in context
        c.Set("session", session)
        c.Set("session_id", cookie.Value)
        c.Set("user_id", session.UserID)
        c.Set("username", session.Username)
        c.Set("csrf_token", session.CSRFToken)

        c.Next()
    }
}

func (sm *SessionManager) RequireAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        session, exists := c.Get("session")
        if !exists || session == nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
            c.Abort()
            return
        }
        c.Next()
    }
}

func (sm *SessionManager) RequirePermission(permission string) gin.HandlerFunc {
    return func(c *gin.Context) {
        sessionInterface, exists := c.Get("session")
        if !exists || sessionInterface == nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
            c.Abort()
            return
        }

        session := sessionInterface.(*SessionData)
        hasPermission := false
        for _, p := range session.Permissions {
            if p == permission {
                hasPermission = true
                break
            }
        }

        if !hasPermission {
            c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
            c.Abort()
            return
        }

        c.Next()
    }
}

// Cookie management
func (sm *SessionManager) SetSessionCookie(c *gin.Context, sessionID string) {
    cookie := &http.Cookie{
        Name:     sm.config.CookieName,
        Value:    sessionID,
        Path:     "/",
        Domain:   sm.config.CookieDomain,
        MaxAge:   int(sm.config.TTL.Seconds()),
        Secure:   sm.config.SecureCookies,
        HttpOnly: true,
        SameSite: http.SameSiteLaxMode,
    }
    http.SetCookie(c.Writer, cookie)
}

func (sm *SessionManager) ClearSessionCookie(c *gin.Context) {
    cookie := &http.Cookie{
        Name:     sm.config.CookieName,
        Value:    "",
        Path:     "/",
        Domain:   sm.config.CookieDomain,
        MaxAge:   -1,
        Secure:   sm.config.SecureCookies,
        HttpOnly: true,
        SameSite: http.SameSiteLaxMode,
    }
    http.SetCookie(c.Writer, cookie)
}

// Demo application
func main() {
    // Setup Redis
    client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
    defer client.Close()

    // Configure session manager
    sessionManager := NewSessionManager(client, SessionConfig{
        TTL:           30 * time.Minute,
        CookieName:    "app_session",
        CookieDomain:  "localhost",
        SecureCookies: false, // Use true in production with HTTPS
        CSRFEnabled:   true,
    })

    // Setup Gin router
    r := gin.Default()
    r.Use(sessionManager.SessionMiddleware())

    // Authentication routes
    r.POST("/login", loginHandler(sessionManager))
    r.POST("/logout", sessionManager.RequireAuth(), logoutHandler(sessionManager))

    // Protected routes
    protected := r.Group("/api")
    protected.Use(sessionManager.RequireAuth())
    {
        protected.GET("/profile", profileHandler)
    }

    // Admin routes
    admin := r.Group("/admin")
    admin.Use(sessionManager.RequireAuth())
    admin.Use(sessionManager.RequirePermission("admin"))
    {
        admin.GET("/users", adminHandler)
    }

    log.Println("Server starting on :8080...")
    r.Run(":8080")
}

// Handlers
func loginHandler(sm *SessionManager) gin.HandlerFunc {
    return func(c *gin.Context) {
        var req struct {
            Username string `json:"username" binding:"required"`
            Password string `json:"password" binding:"required"`
        }

        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        // TODO: Validate credentials against database
        if req.Username != "demo" || req.Password != "password" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
            return
        }

        // Create session with permissions
        permissions := []string{"read", "write"}
        if req.Username == "admin" {
            permissions = append(permissions, "admin")
        }

        sessionID, session, err := sm.CreateSession(123, req.Username, "demo@example.com", "user", permissions)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
            return
        }

        sm.SetSessionCookie(c, sessionID)

        c.JSON(http.StatusOK, gin.H{
            "message":     "Login successful",
            "user_id":     session.UserID,
            "username":    session.Username,
            "permissions": session.Permissions,
            "csrf_token":  session.CSRFToken,
        })
    }
}

func logoutHandler(sm *SessionManager) gin.HandlerFunc {
    return func(c *gin.Context) {
        sessionID, _ := c.Get("session_id")
        if sessionID != nil {
            sm.DeleteSession(sessionID.(string))
        }
        sm.ClearSessionCookie(c)
        c.JSON(http.StatusOK, gin.H{"message": "Logout successful"})
    }
}

func profileHandler(c *gin.Context) {
    session := c.MustGet("session").(*SessionData)
    c.JSON(http.StatusOK, gin.H{
        "user_id":       session.UserID,
        "username":      session.Username,
        "email":         session.Email,
        "role":          session.Role,
        "login_time":    session.LoginTime,
        "last_activity": session.LastActivity,
        "permissions":   session.Permissions,
    })
}

func adminHandler(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "message": "Admin panel access granted",
        "users":   []string{"user1", "user2", "admin"},
    })
}

5. Hands-On Practice (30-40 minutes)

Exercise 1: Multi-Device Session Management (15 minutes)

Objective: Track and manage multiple concurrent sessions per user

Requirements:

  • Allow users to log in from multiple devices
  • Show active sessions with device info
  • Provide “log out from all devices” functionality
  • Implement session limits per user

Exercise 2: Session Analytics and Monitoring (15 minutes)

Objective: Implement session tracking and analytics

Requirements:

  • Track login events and patterns
  • Monitor active session counts
  • Generate daily login statistics
  • Detect suspicious session activity

Exercise 3: Advanced Security Features (10 minutes)

Objective: Implement additional security measures

Requirements:

  • Session rotation after privilege escalation
  • IP address validation
  • Suspicious activity detection
  • Automatic logout after inactivity

6. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Issues

  1. Session Fixation Attacks

    • Problem: Using same session ID before and after login
    • Solution: Generate new session ID after authentication
  2. Memory Leaks from Expired Sessions

    • Problem: Sessions not being cleaned up properly
    • Solution: Implement cleanup job and proper TTL
  3. Race Conditions in Session Updates

    • Problem: Concurrent session modifications
    • Solution: Use Redis transactions or Lua scripts

7. Best Practices and Professional Tips (5 minutes)

Production Checklist

  • ✅ Use HTTPS with secure cookies in production
  • ✅ Implement session rotation after privilege escalation
  • ✅ Set appropriate session timeouts based on sensitivity
  • ✅ Monitor session analytics and detect anomalies
  • ✅ Implement proper session cleanup and garbage collection
  • ✅ Use Redis persistence (RDB/AOF) for session durability
  • ✅ Consider Redis Cluster for high availability
  • ✅ Implement rate limiting on login attempts

8. Summary and Review (5 minutes)

Key Takeaways

  1. Redis provides distributed session storage enabling horizontal scaling
  2. Security is paramount - use secure IDs, HTTPS, and CSRF protection
  3. Session lifecycle management prevents memory leaks and security issues
  4. Middleware patterns provide clean separation of concerns
  5. Monitoring and analytics help detect issues and optimize performance

Production Architecture Pattern

[Load Balancer] → [App Servers] → [Redis Cluster]
       ↓              ↓              ↓
   SSL Termination  Session      Persistence
   Rate Limiting    Middleware   Replication
   Health Checks    CSRF         Monitoring

Quick Review Questions

  1. Why is Redis better than file-based sessions for distributed applications?
  2. How do you prevent session fixation attacks?
  3. What’s the difference between secure and HttpOnly cookie flags?
  4. How would you implement “remember me” functionality securely?
  5. What monitoring metrics are important for session management?

For Instructors

Teaching Tips

  • Demonstrate session hijacking to show security importance
  • Use browser dev tools to show cookie behavior
  • Show Redis CLI to examine session data structure
  • Demonstrate load balancing with multiple app instances
  • Highlight the difference between authentication and authorization

Common Student Questions

  • Q: “Should I store user data directly in sessions?” A: Store minimal data, use session for identity and fetch details from database

  • Q: “How long should sessions last?” A: Balance security vs UX - shorter for sensitive apps, extend on activity

  • Q: “What about JWT vs Redis sessions?” A: JWT for stateless APIs, Redis sessions for web apps needing server control