CSRF Double-Submit Token Middleware

Issue a random CSRF cookie on safe requests and require a matching header on unsafe methods using constant-time comparison.

GoSecurity Boundaries

Code

snippet
package main

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/base64"
	"fmt"
	"net/http"
)

const (
	csrfCookie = "csrf_token"
	csrfHeader = "X-CSRF-Token"
)

func newToken() (string, error) {
	buffer := make([]byte, 32)
	if _, err := rand.Read(buffer); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(buffer), nil
}

func CSRFMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cookie, cookieErr := r.Cookie(csrfCookie)
		switch r.Method {
		case http.MethodGet, http.MethodHead, http.MethodOptions:
			if cookieErr != nil {
				token, err := newToken()
				if err != nil {
					http.Error(w, "cannot issue csrf token", http.StatusInternalServerError)
					return
				}
				http.SetCookie(w, &http.Cookie{
					Name:     csrfCookie,
					Value:    token,
					Path:     "/",
					HttpOnly: false,
					SameSite: http.SameSiteLaxMode,
					Secure:   r.TLS != nil,
				})
			}
			next.ServeHTTP(w, r)
			return
		}

		if cookieErr != nil {
			http.Error(w, "missing csrf cookie", http.StatusForbidden)
			return
		}
		header := r.Header.Get(csrfHeader)
		if subtle.ConstantTimeCompare([]byte(header), []byte(cookie.Value)) != 1 {
			http.Error(w, "invalid csrf token", http.StatusForbidden)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	handler := CSRFMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "accepted")
	}))
	http.Handle("/submit", handler)
	_ = http.ListenAndServe(":8080", nil)
}