package httpserver import ( "context" "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "net/http" "strings" "time" ) const ( adminCookiePath = "/admin" csrfFormField = "csrf_token" csrfHeaderName = "X-CSRF-Token" ) type csrfTokenContextKey struct{} func (h *handler) adminCSRF(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token, cookie, err := h.ensureCSRFCookie(r) if err != nil { http.Error(w, "csrf setup failed", http.StatusInternalServerError) return } if cookie != nil { http.SetCookie(w, cookie) } r = r.WithContext(context.WithValue(r.Context(), csrfTokenContextKey{}, token)) if requiresCSRFProtection(r.Method) { submittedToken, err := h.submittedCSRFToken(w, r) if err != nil || subtle.ConstantTimeCompare([]byte(token), []byte(submittedToken)) != 1 { http.Error(w, "csrf validation failed", http.StatusForbidden) return } } next.ServeHTTP(w, r) }) } func (h *handler) csrfToken(r *http.Request) string { if token, ok := r.Context().Value(csrfTokenContextKey{}).(string); ok { return token } cookie, err := r.Cookie(h.config.CSRFCookieName) if err != nil { return "" } if !validCSRFCookieToken(cookie.Value) { return "" } return cookie.Value } func (h *handler) issueCSRFCookie(w http.ResponseWriter) (string, error) { token, err := generateCSRFToken() if err != nil { return "", err } http.SetCookie(w, h.csrfCookie(token)) return token, nil } func (h *handler) clearCSRFCookie() *http.Cookie { return &http.Cookie{ Name: h.config.CSRFCookieName, Value: "", Path: adminCookiePath, HttpOnly: true, SameSite: http.SameSiteStrictMode, Secure: h.config.SecureCookies, Expires: time.Unix(0, 0).UTC(), MaxAge: -1, } } func (h *handler) ensureCSRFCookie(r *http.Request) (string, *http.Cookie, error) { cookie, err := r.Cookie(h.config.CSRFCookieName) if err == nil && validCSRFCookieToken(cookie.Value) { return cookie.Value, nil, nil } token, err := generateCSRFToken() if err != nil { return "", nil, fmt.Errorf("generate csrf token: %w", err) } return token, h.csrfCookie(token), nil } func (h *handler) csrfCookie(token string) *http.Cookie { return &http.Cookie{ Name: h.config.CSRFCookieName, Value: token, Path: adminCookiePath, HttpOnly: true, SameSite: http.SameSiteStrictMode, Secure: h.config.SecureCookies, } } func (h *handler) submittedCSRFToken(w http.ResponseWriter, r *http.Request) (string, error) { if token := strings.TrimSpace(r.Header.Get(csrfHeaderName)); token != "" { return token, nil } contentType := strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type"))) if strings.HasPrefix(contentType, "multipart/form-data") { r.Body = http.MaxBytesReader(w, r.Body, maxUploadRequestLimit(h.config.MaxUploadBytes)) if err := r.ParseMultipartForm(16 << 20); err != nil { return "", err } return strings.TrimSpace(r.FormValue(csrfFormField)), nil } if err := r.ParseForm(); err != nil { return "", err } return strings.TrimSpace(r.FormValue(csrfFormField)), nil } func generateCSRFToken() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(buf), nil } func requiresCSRFProtection(method string) bool { switch method { case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace: return false default: return true } } func validCSRFCookieToken(token string) bool { token = strings.TrimSpace(token) return len(token) >= 32 }