auth/internal/server/session.go

101 lines
2.4 KiB
Go

package server
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"time"
)
const (
sessionCookieName = "auth_session"
sessionLifetime = 12 * time.Hour
sessionSecretMinLength = 32
csrfTokenByteLength int = 32
)
// SessionStore persists session data using secure HTTP cookies.
type SessionStore struct {
secret []byte
}
// NewSessionStore creates a cookie-backed session store.
func NewSessionStore(secret []byte) (*SessionStore, error) {
if len(secret) < sessionSecretMinLength {
return nil, fmt.Errorf("session secret must be at least %d bytes", sessionSecretMinLength)
}
// copy secret to avoid external mutation
buf := make([]byte, len(secret))
copy(buf, secret)
return &SessionStore{secret: buf}, nil
}
// SessionState holds per-request session data after loading.
type SessionState struct {
Authenticated bool
Email string
CSRFToken string
}
// Load extracts session data from the request cookies.
func (s *SessionStore) Load(r *http.Request) SessionState {
c, err := r.Cookie(sessionCookieName)
if err != nil {
return SessionState{}
}
payload, err := decodeSession(c.Value, s.secret)
if err != nil {
return SessionState{}
}
return payload
}
// Save persists the session state onto the response cookies.
func (s *SessionStore) Save(w http.ResponseWriter, state SessionState) error {
serialized, err := encodeSession(state, s.secret)
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: serialized,
Path: "/",
HttpOnly: true,
Secure: false, // TODO: in production, set to true
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(sessionLifetime),
})
return nil
}
// Clear removes the session cookie from the client.
func (s *SessionStore) Clear(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
MaxAge: -1,
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteLaxMode,
})
}
// ensureCSRFToken returns a session state with a CSRF token present.
func ensureCSRFToken(state SessionState) (SessionState, error) {
if state.CSRFToken != "" {
return state, nil
}
token := make([]byte, csrfTokenByteLength)
if _, err := rand.Read(token); err != nil {
return state, err
}
state.CSRFToken = base64.RawURLEncoding.EncodeToString(token)
return state, nil
}