package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "net/http" ) const csrfCookieName = "vibesonic_csrf" // CSRF implements the double-submit-cookie pattern: a random token is set // in a cookie readable by JS-free forms via a template field, and checked // against the same value submitted in the form body on state-changing // requests. type CSRF struct { CookieSecure bool } // Token returns the CSRF token for this request, setting a fresh cookie // if none exists yet. func (c *CSRF) Token(w http.ResponseWriter, r *http.Request) string { if ck, err := r.Cookie(csrfCookieName); err == nil && ck.Value != "" { return ck.Value } b := make([]byte, 32) _, _ = rand.Read(b) token := base64.RawURLEncoding.EncodeToString(b) http.SetCookie(w, &http.Cookie{ Name: csrfCookieName, Value: token, Path: "/", HttpOnly: false, // must be readable by the template render, not JS; kept simple since it never leaves same-origin form submits Secure: c.CookieSecure, SameSite: http.SameSiteLaxMode, }) return token } // Protect wraps a handler, rejecting POST/PUT/PATCH/DELETE requests whose // csrf_token form value doesn't match the csrf cookie. func (c *CSRF) Protect(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete: ck, err := r.Cookie(csrfCookieName) if err != nil || ck.Value == "" { http.Error(w, "403 Forbidden: missing CSRF cookie", http.StatusForbidden) return } submitted := r.FormValue("csrf_token") if subtle.ConstantTimeCompare([]byte(submitted), []byte(ck.Value)) != 1 { http.Error(w, "403 Forbidden: invalid CSRF token", http.StatusForbidden) return } } next.ServeHTTP(w, r) }) }