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}