package auth import ( "net/http" "net/http/httptest" "testing" ) func csrfChain(t *testing.T, trusted ...string) (http.Handler, *bool) { t.Helper() passed := false deny := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusForbidden) }) mw, err := CSRF(trusted, CookieName, deny, deny) if err != nil { t.Fatal(err) } return mw(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { passed = true })), &passed } func TestCSRF(t *testing.T) { cases := []struct { name string method string headers map[string]string cookie bool trusted []string want int }{ {name: "same-origin write with the client header", method: http.MethodPost, cookie: true, headers: map[string]string{"Sec-Fetch-Site": "same-origin", ClientHeader: "web"}, want: http.StatusOK}, {name: "cross-site write rejected by the stdlib guard", method: http.MethodPost, cookie: true, headers: map[string]string{"Sec-Fetch-Site": "cross-site", ClientHeader: "web"}, want: http.StatusForbidden}, // The hole the stdlib guard leaves open on purpose: no Sec-Fetch-Site, no Origin. A // pre-2023 browser posting a cross-site multipart form looks exactly like this. {name: "headerless cookie write rejected by the client header", method: http.MethodPost, cookie: true, want: http.StatusForbidden}, {name: "bearer write needs no client header", method: http.MethodPost, headers: map[string]string{"Authorization": "Bearer t"}, want: http.StatusOK}, {name: "reads are never blocked", method: http.MethodGet, cookie: true, want: http.StatusOK}, {name: "trusted origin allowed", method: http.MethodPost, cookie: true, trusted: []string{"https://app.example.org"}, headers: map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://app.example.org", ClientHeader: "web"}, want: http.StatusOK}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { h, passed := csrfChain(t, tc.trusted...) r := httptest.NewRequest(tc.method, "/v0/books", nil) for k, v := range tc.headers { r.Header.Set(k, v) } if tc.cookie { r.AddCookie(&http.Cookie{Name: CookieName, Value: "t"}) } w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != tc.want { t.Fatalf("status = %d, want %d", w.Code, tc.want) } if got := *passed; got != (tc.want == http.StatusOK) { t.Fatalf("handler reached = %v", got) } }) } } func TestCSRFRejectsAMalformedTrustedOrigin(t *testing.T) { if _, err := CSRF([]string{"app.example.org"}, CookieName, http.NotFoundHandler(), http.NotFoundHandler()); err == nil { t.Fatal("an origin without a scheme must be refused at boot, not at request time") } }