package web import ( "crypto/hmac" "crypto/rand" "encoding/base64" "errors" "fmt" "net/http" "tug/internal/transport/web/text" ) const ( csrfField = "csrf" csrfCookie = "__Host-authsvc_csrf" csrfSecretSize = 32 maxFormBytes = 64 << 10 ) var ( errCSRF = errors.New("csrf token rejected") errForm = errors.New("form body does not parse") ) func setCookie(w http.ResponseWriter, name, value string, maxAge int) { http.SetCookie(w, &http.Cookie{ Name: name, Value: value, Path: "/", MaxAge: maxAge, Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) } func (k *Kit) csrfSecret(r *http.Request) ([]byte, bool) { c, err := r.Cookie(csrfCookie) if err != nil { return nil, false } b, err := base64.RawURLEncoding.Strict().DecodeString(c.Value) if err != nil || len(b) != csrfSecretSize { return nil, false } return b, true } func (k *Kit) newCSRFSecret(w http.ResponseWriter) []byte { secret := make([]byte, csrfSecretSize) rand.Read(secret) setCookie(w, csrfCookie, base64.RawURLEncoding.EncodeToString(secret), 0) return secret } func (k *Kit) RotateCSRF(w http.ResponseWriter) { k.newCSRFSecret(w) } func (k *Kit) csrfMAC(secret []byte, r *http.Request) []byte { return mac(k.keys.csrf, secret, []byte(sessionID(r))) } func (k *Kit) csrfToken(w http.ResponseWriter, r *http.Request) string { secret, ok := k.csrfSecret(r) if !ok { secret = k.newCSRFSecret(w) } return base64.RawURLEncoding.EncodeToString(k.csrfMAC(secret, r)) } func (k *Kit) checkCSRF(w http.ResponseWriter, r *http.Request) error { if err := k.cop.Check(r); err != nil { return err } r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes) if err := r.ParseForm(); err != nil { return fmt.Errorf("%w: %w", errForm, err) } secret, ok := k.csrfSecret(r) if !ok { return errCSRF } got, err := base64.RawURLEncoding.Strict().DecodeString(r.PostFormValue(csrfField)) if err != nil || !hmac.Equal(got, k.csrfMAC(secret, r)) { return errCSRF } return nil } func (k *Kit) protect(next, fallback http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := k.checkCSRF(w, r); err != nil { if fallback != nil && errors.Is(err, errCSRF) { fallback.ServeHTTP(w, r) return } k.renderError(w, r, http.StatusForbidden, text.ErrorForbiddenTitle, text.ErrorFormExpired) return } next.ServeHTTP(w, r) }) }