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:
- Session Fundamentals - Sessions, cookies, security considerations, and Redis advantages
- Redis Session Store - Implementing distributed session backend with Go
- Session Middleware - Building reusable authentication and authorization middleware
- Security Best Practices - CSRF protection, secure tokens, session hijacking prevention
- Advanced Patterns - Multi-device sessions, SSO, and cross-domain session sharing
- 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
-
Session Fixation Attacks
- Problem: Using same session ID before and after login
- Solution: Generate new session ID after authentication
-
Memory Leaks from Expired Sessions
- Problem: Sessions not being cleaned up properly
- Solution: Implement cleanup job and proper TTL
-
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
- Redis provides distributed session storage enabling horizontal scaling
- Security is paramount - use secure IDs, HTTPS, and CSRF protection
- Session lifecycle management prevents memory leaks and security issues
- Middleware patterns provide clean separation of concerns
- 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
- Why is Redis better than file-based sessions for distributed applications?
- How do you prevent session fixation attacks?
- What’s the difference between secure and HttpOnly cookie flags?
- How would you implement “remember me” functionality securely?
- 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