package middleware import ( "crypto/rand" "crypto/subtle" "encoding/base64" "net/http" ) const csrfCookieName = "fb_csrf" const csrfHeaderName = "X-CSRF-Token" // NewCSRFToken generates a cryptographically random CSRF token, sets it as a // non-HttpOnly cookie (so the SPA can read it), and returns the token value. func NewCSRFToken(w http.ResponseWriter, secure bool) (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } token := base64.RawURLEncoding.EncodeToString(b) http.SetCookie(w, &http.Cookie{ Name: csrfCookieName, Value: token, Path: "/", HttpOnly: false, // must be readable by JS for the double-submit pattern Secure: secure, SameSite: http.SameSiteLaxMode, }) return token, nil } // CSRF is a middleware that enforces the double-submit cookie pattern for all // state-mutating requests (POST, PUT, PATCH, DELETE). Safe methods are passed // through unchanged. func CSRF(secure bool) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace: // Safe method — no CSRF validation needed, just pass through. next.ServeHTTP(w, r) return } cookie, err := r.Cookie(csrfCookieName) if err != nil || cookie.Value == "" { http.Error(w, `{"error":"CSRF cookie missing"}`, http.StatusForbidden) return } headerToken := r.Header.Get(csrfHeaderName) if headerToken == "" { http.Error(w, `{"error":"X-CSRF-Token header missing"}`, http.StatusForbidden) return } // Constant-time compare prevents timing attacks. if subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(headerToken)) != 1 { http.Error(w, `{"error":"CSRF validation failed"}`, http.StatusForbidden) return } next.ServeHTTP(w, r) }) } }