textmachine/platform/internal/auth/csrf_test.go

71 lines
2.6 KiB
Go

package auth
import (
"net/http"
"net/http/httptest"
"testing"
)
func csrfChain(t *testing.T, trusted ...string) (http.Handler, *bool) {
t.Helper()
passed := false
mw, err := CSRF(trusted, CookieName, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
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()); err == nil {
t.Fatal("an origin without a scheme must be refused at boot, not at request time")
}
}