Raw
  1package web
  2
  3import (
  4	"crypto/hmac"
  5	"crypto/rand"
  6	"encoding/base64"
  7	"errors"
  8	"fmt"
  9	"net/http"
 10
 11	"tug/internal/transport/web/text"
 12)
 13
 14const (
 15	csrfField      = "csrf"
 16	csrfCookie     = "__Host-authsvc_csrf"
 17	csrfSecretSize = 32
 18	maxFormBytes   = 64 << 10
 19)
 20
 21var (
 22	errCSRF = errors.New("csrf token rejected")
 23	errForm = errors.New("form body does not parse")
 24)
 25
 26func setCookie(w http.ResponseWriter, name, value string, maxAge int) {
 27	http.SetCookie(w, &http.Cookie{
 28		Name:     name,
 29		Value:    value,
 30		Path:     "/",
 31		MaxAge:   maxAge,
 32		Secure:   true,
 33		HttpOnly: true,
 34		SameSite: http.SameSiteLaxMode,
 35	})
 36}
 37
 38func (k *Kit) csrfSecret(r *http.Request) ([]byte, bool) {
 39	c, err := r.Cookie(csrfCookie)
 40	if err != nil {
 41		return nil, false
 42	}
 43	b, err := base64.RawURLEncoding.Strict().DecodeString(c.Value)
 44	if err != nil || len(b) != csrfSecretSize {
 45		return nil, false
 46	}
 47	return b, true
 48}
 49
 50func (k *Kit) newCSRFSecret(w http.ResponseWriter) []byte {
 51	secret := make([]byte, csrfSecretSize)
 52	rand.Read(secret)
 53	setCookie(w, csrfCookie, base64.RawURLEncoding.EncodeToString(secret), 0)
 54	return secret
 55}
 56
 57func (k *Kit) RotateCSRF(w http.ResponseWriter) {
 58	k.newCSRFSecret(w)
 59}
 60
 61func (k *Kit) csrfMAC(secret []byte, r *http.Request) []byte {
 62	return mac(k.keys.csrf, secret, []byte(sessionID(r)))
 63}
 64
 65func (k *Kit) csrfToken(w http.ResponseWriter, r *http.Request) string {
 66	secret, ok := k.csrfSecret(r)
 67	if !ok {
 68		secret = k.newCSRFSecret(w)
 69	}
 70	return base64.RawURLEncoding.EncodeToString(k.csrfMAC(secret, r))
 71}
 72
 73func (k *Kit) checkCSRF(w http.ResponseWriter, r *http.Request) error {
 74	if err := k.cop.Check(r); err != nil {
 75		return err
 76	}
 77	r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
 78	if err := r.ParseForm(); err != nil {
 79		return fmt.Errorf("%w: %w", errForm, err)
 80	}
 81	secret, ok := k.csrfSecret(r)
 82	if !ok {
 83		return errCSRF
 84	}
 85	got, err := base64.RawURLEncoding.Strict().DecodeString(r.PostFormValue(csrfField))
 86	if err != nil || !hmac.Equal(got, k.csrfMAC(secret, r)) {
 87		return errCSRF
 88	}
 89	return nil
 90}
 91
 92func (k *Kit) protect(next, fallback http.Handler) http.Handler {
 93	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 94		if err := k.checkCSRF(w, r); err != nil {
 95			if fallback != nil && errors.Is(err, errCSRF) {
 96				fallback.ServeHTTP(w, r)
 97				return
 98			}
 99			k.renderError(w, r, http.StatusForbidden, text.ErrorForbiddenTitle, text.ErrorFormExpired)
100			return
101		}
102		next.ServeHTTP(w, r)
103	})
104}