1. go
  2. /web
  3. /sessions

Managing Sessions in Go Web Applications

Session management is crucial for maintaining user state in web applications. This guide covers how to implement and manage sessions effectively in Go.

Basic Session Management

Using gorilla/sessions

package main

import (
    "github.com/gorilla/sessions"
    "net/http"
)

// Create store
var store = sessions.NewCookieStore([]byte("secret-key"))

func sessionHandler(w http.ResponseWriter, r *http.Request) {
    // Get session
    session, err := store.Get(r, "session-name")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    // Set value
    session.Values["user_id"] = "123"
    
    // Save session
    err = session.Save(r, w)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

Custom Session Manager

type Session struct {
    ID        string
    UserID    string
    CreatedAt time.Time
    ExpiresAt time.Time
    Data      map[string]interface{}
}

type SessionManager struct {
    store  map[string]*Session
    mu     sync.RWMutex
    maxAge time.Duration
}

func NewSessionManager(maxAge time.Duration) *SessionManager {
    return &SessionManager{
        store:  make(map[string]*Session),
        maxAge: maxAge,
    }
}

func (sm *SessionManager) CreateSession() *Session {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    
    session := &Session{
        ID:        generateID(),
        CreatedAt: time.Now(),
        ExpiresAt: time.Now().Add(sm.maxAge),
        Data:      make(map[string]interface{}),
    }
    
    sm.store[session.ID] = session
    return session
}

Storage Backends

Redis Session Store

type RedisStore struct {
    client *redis.Client
    prefix string
}

func NewRedisStore(client *redis.Client) *RedisStore {
    return &RedisStore{
        client: client,
        prefix: "session:",
    }
}

func (rs *RedisStore) Save(session *Session) error {
    data, err := json.Marshal(session)
    if err != nil {
        return err
    }
    
    key := rs.prefix + session.ID
    expiration := time.Until(session.ExpiresAt)
    
    return rs.client.Set(context.Background(),
        key, data, expiration).Err()
}

func (rs *RedisStore) Get(id string) (*Session, error) {
    key := rs.prefix + id
    data, err := rs.client.Get(context.Background(), key).Bytes()
    if err != nil {
        if err == redis.Nil {
            return nil, nil
        }
        return nil, err
    }
    
    var session Session
    err = json.Unmarshal(data, &session)
    return &session, err
}

Database Session Store

type DBStore struct {
    db *sql.DB
}

func NewDBStore(db *sql.DB) *DBStore {
    return &DBStore{db: db}
}

func (ds *DBStore) Save(session *Session) error {
    query := `
        INSERT INTO sessions (id, user_id, data, expires_at)
        VALUES ($1, $2, $3, $4)
        ON CONFLICT (id) DO UPDATE
        SET data = $3, expires_at = $4
    `
    
    data, err := json.Marshal(session.Data)
    if err != nil {
        return err
    }
    
    _, err = ds.db.Exec(query,
        session.ID,
        session.UserID,
        data,
        session.ExpiresAt,
    )
    return err
}

Session Middleware

Basic Session Middleware

func SessionMiddleware(manager *SessionManager) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Get session ID from cookie
            cookie, err := r.Cookie("session_id")
            if err != nil {
                // Create new session
                session := manager.CreateSession()
                cookie = &http.Cookie{
                    Name:     "session_id",
                    Value:    session.ID,
                    Expires:  session.ExpiresAt,
                    HttpOnly: true,
                    Secure:   true,
                    SameSite: http.SameSiteStrictMode,
                }
                http.SetCookie(w, cookie)
            }
            
            // Get session
            session, err := manager.Get(cookie.Value)
            if err != nil {
                http.Error(w, "Invalid session", http.StatusUnauthorized)
                return
            }
            
            // Add session to context
            ctx := context.WithValue(r.Context(), "session", session)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Secure Session Middleware

type SecureSessionManager struct {
    store      SessionStore
    encryption *Encryption
}

func (sm *SecureSessionManager) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Get encrypted session ID
        cookie, err := r.Cookie("session_id")
        if err != nil {
            http.Error(w, "No session", http.StatusUnauthorized)
            return
        }
        
        // Decrypt session ID
        sessionID, err := sm.encryption.Decrypt(cookie.Value)
        if err != nil {
            http.Error(w, "Invalid session", http.StatusUnauthorized)
            return
        }
        
        // Get and validate session
        session, err := sm.store.Get(sessionID)
        if err != nil || session.IsExpired() {
            http.Error(w, "Invalid session", http.StatusUnauthorized)
            return
        }
        
        // Add session to context
        ctx := context.WithValue(r.Context(), "session", session)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Best Practices

1. Session Security

type SessionConfig struct {
    Secret    []byte
    MaxAge    time.Duration
    Secure    bool
    HttpOnly  bool
    SameSite  http.SameSite
    Domain    string
    Path      string
}

func NewSecureSession(config SessionConfig) *Session {
    return &Session{
        ID:        generateSecureID(),
        CreatedAt: time.Now(),
        ExpiresAt: time.Now().Add(config.MaxAge),
        Cookie: &http.Cookie{
            Name:     "session_id",
            Value:    "",  // Set later
            MaxAge:   int(config.MaxAge.Seconds()),
            Secure:   config.Secure,
            HttpOnly: config.HttpOnly,
            SameSite: config.SameSite,
            Domain:   config.Domain,
            Path:     config.Path,
        },
    }
}

func generateSecureID() string {
    b := make([]byte, 32)
    if _, err := rand.Read(b); err != nil {
        panic(err)
    }
    return base64.URLEncoding.EncodeToString(b)
}

2. Session Cleanup

type SessionCleaner struct {
    store  SessionStore
    ticker *time.Ticker
    done   chan bool
}

func NewSessionCleaner(store SessionStore, interval time.Duration) *SessionCleaner {
    return &SessionCleaner{
        store:  store,
        ticker: time.NewTicker(interval),
        done:   make(chan bool),
    }
}

func (sc *SessionCleaner) Start() {
    go func() {
        for {
            select {
            case <-sc.ticker.C:
                sc.cleanup()
            case <-sc.done:
                sc.ticker.Stop()
                return
            }
        }
    }()
}

func (sc *SessionCleaner) cleanup() {
    if err := sc.store.DeleteExpired(); err != nil {
        log.Printf("Session cleanup error: %v", err)
    }
}

3. Session Data Validation

type SessionData struct {
    UserID    string    `json:"user_id"`
    Username  string    `json:"username"`
    LastSeen  time.Time `json:"last_seen"`
    UserAgent string    `json:"user_agent"`
}

func (sd *SessionData) Validate() error {
    if sd.UserID == "" {
        return errors.New("user_id is required")
    }
    if sd.Username == "" {
        return errors.New("username is required")
    }
    if sd.LastSeen.IsZero() {
        return errors.New("last_seen is required")
    }
    return nil
}

func (s *Session) SetData(data *SessionData) error {
    if err := data.Validate(); err != nil {
        return err
    }
    s.Data = data
    return nil
}

Common Patterns

1. Session Authentication

func AuthMiddleware(sm *SessionManager) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            session := r.Context().Value("session").(*Session)
            
            // Check if user is authenticated
            if session.Data["authenticated"] != true {
                http.Redirect(w, r, "/login", http.StatusSeeOther)
                return
            }
            
            // Update last seen
            session.Data["last_seen"] = time.Now()
            if err := sm.Save(session); err != nil {
                http.Error(w, "Session error", http.StatusInternalServerError)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

2. Session Rate Limiting

type SessionLimiter struct {
    store    SessionStore
    maxReqs  int
    window   time.Duration
}

func (sl *SessionLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        session := r.Context().Value("session").(*Session)
        
        // Get request history
        reqs, ok := session.Data["requests"].([]time.Time)
        if !ok {
            reqs = make([]time.Time, 0)
        }
        
        // Remove old requests
        now := time.Now()
        windowStart := now.Add(-sl.window)
        for i, t := range reqs {
            if t.After(windowStart) {
                reqs = reqs[i:]
                break
            }
        }
        
        // Check rate limit
        if len(reqs) >= sl.maxReqs {
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        
        // Add new request
        reqs = append(reqs, now)
        session.Data["requests"] = reqs
        
        next.ServeHTTP(w, r)
    })
}

Next Steps