package auth import ( "fmt" "net/http" ) // ClientHeader is the header a browser client must send with every unsafe cookie-authenticated // request. Any value; its PRESENCE is the assertion. const ClientHeader = "X-TM-Client" // CSRF guards the cookie path and only it: a Bearer token is not ambient authority, so no // cross-site page can make a browser attach one. // // Two layers, both cheap: // // - http.CrossOriginProtection (stdlib, Go 1.25+) — Sec-Fetch-Site with an Origin/Host fallback. // This is the mechanism STACK_DECISIONS §5 describes, and it now ships with the toolchain, so // we do not hand-roll it. // - a required custom header on unsafe requests that carry the session cookie. The stdlib check // ALLOWS a request bearing neither Sec-Fetch-Site nor Origin, on the reasoning that it is not // a browser. A pre-2023 browser posting a cross-site
is exactly that case, and // POST /books takes multipart/form-data — a form-reachable content type that triggers no // preflight. A plain form cannot set a custom header; a fetch() from our own origin can. // // trustedOrigins are additional origins allowed to make unsafe requests (a separately deployed // frontend); empty means same-origin only. func CSRF(trustedOrigins []string, cookieName string, deny http.Handler) (func(http.Handler) http.Handler, error) { p := http.NewCrossOriginProtection() p.SetDenyHandler(deny) for _, o := range trustedOrigins { if err := p.AddTrustedOrigin(o); err != nil { return nil, fmt.Errorf("auth: trusted origin %q: %w", o, err) } } return func(next http.Handler) http.Handler { return p.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if cookieUnsafe(r, cookieName) && r.Header.Get(ClientHeader) == "" { deny.ServeHTTP(w, r) return } next.ServeHTTP(w, r) })) }, nil } // cookieUnsafe reports a state-changing request presented by cookie. It reads the cookie directly // rather than the principal: this check runs BEFORE authentication, so that a forged request is // refused without touching the session table. func cookieUnsafe(r *http.Request, cookieName string) bool { switch r.Method { case http.MethodGet, http.MethodHead, http.MethodOptions: return false } // A well-formed Bearer is exempt. Present PARSES it; it does not validate it — the session // lookup does that, later — so `Bearer ` gets the exemption too. That is safe, and // the reason is worth naming because it is not the parsing: once an Authorization header is // present, Present NEVER falls back to the cookie, so such a request authenticates as nothing // and ends in 401. It cannot trade the CSRF check for the cookie's authority; it can only give // up its own. Testing for a merely non-empty header would be weaker still — `Authorization: x` // would let the caller pick which layer applies (PD-33, PD-50). if _, _, ok := Present(r, ""); ok && r.Header.Get("Authorization") != "" { return false } c, err := r.Cookie(cookieName) return err == nil && c.Value != "" }