package web // The form token bound to the session (novox/hq ADR 0440 ยง3, issue 1284). There is no write page to test // against yet (issue 1267 builds it); these tests stand in for it with a minimal changing handler ("send"), // proving the mechanism issue 1267 will wire mail_send behind: a session, and that session's own token. import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" ) // sessionAndCookie starts a session directly (bypassing sign-in, which auth_test.go already covers on its // own) and answers it with the cookie a request would carry. func sessionAndCookie(t *testing.T, sessions *Sessions, operator string) (Session, *http.Cookie) { t.Helper() id, err := sessions.Create(operator) if err != nil { t.Fatal(err) } sess, ok := sessions.Get(id) if !ok { t.Fatal("the session just created is not there") } return sess, &http.Cookie{Name: SessionCookie, Value: id} } // 1284/AC1: a signed-in session's own form carries that session's token. func TestFormTokenInputCarriesTheSessionsOwnToken(t *testing.T) { sess, _ := sessionAndCookie(t, NewSessions(nil), "alice") html := string(FormTokenInput(sess)) if !strings.Contains(html, `name="csrf_token"`) || !strings.Contains(html, sess.Token) { t.Fatalf("the form's hidden field does not carry the session's token: %s", html) } } // changingHandler is the stand-in for mail_send (issue 1267 is the real one): it counts how many times it was // actually reached. func changingHandler(calls *int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { *calls++ w.WriteHeader(http.StatusOK) } } func postSend(t *testing.T, handler http.HandlerFunc, cookie *http.Cookie, token string) *httptest.ResponseRecorder { t.Helper() form := url.Values{} if token != "" { form.Set(FormTokenField, token) } r := httptest.NewRequest(http.MethodPost, "/send", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") if cookie != nil { r.AddCookie(cookie) } w := httptest.NewRecorder() handler(w, r) return w } // 1284/AC2: a POST of a send without the token is answered 403, and the change (here, the stand-in) is never // called. func TestPOSTWithoutTheTokenIsRefusedAndTheChangeIsNotCalled(t *testing.T) { sessions := NewSessions(nil) _, cookie := sessionAndCookie(t, sessions, "alice") auth := &Auth{Sessions: sessions} var calls int handler := auth.RequireSession(RequireFormToken(changingHandler(&calls))) w := postSend(t, handler, cookie, "") if w.Code != http.StatusForbidden { t.Fatalf("status %d, want 403", w.Code) } if calls != 0 { t.Fatalf("the change was called %d time(s) with no token", calls) } } // 1284/AC3: a POST carrying another session's token is answered 403, and the change is never called. func TestPOSTWithAnotherSessionsTokenIsRefusedAndTheChangeIsNotCalled(t *testing.T) { sessions := NewSessions(nil) _, cookie := sessionAndCookie(t, sessions, "alice") otherSess, _ := sessionAndCookie(t, sessions, "bob") auth := &Auth{Sessions: sessions} var calls int handler := auth.RequireSession(RequireFormToken(changingHandler(&calls))) w := postSend(t, handler, cookie, otherSess.Token) if w.Code != http.StatusForbidden { t.Fatalf("status %d, want 403", w.Code) } if calls != 0 { t.Fatalf("the change was called %d time(s) with another session's token", calls) } } // 1284/AC4: a POST carrying its own session's token proceeds. func TestPOSTWithItsOwnSessionsTokenProceeds(t *testing.T) { sessions := NewSessions(nil) sess, cookie := sessionAndCookie(t, sessions, "alice") auth := &Auth{Sessions: sessions} var calls int handler := auth.RequireSession(RequireFormToken(changingHandler(&calls))) w := postSend(t, handler, cookie, sess.Token) if w.Code != http.StatusOK { t.Fatalf("status %d, want 200: %s", w.Code, w.Body.String()) } if calls != 1 { t.Fatalf("the change was called %d time(s), want 1", calls) } } // No session at all: RequireSession's own redirect runs, RequireFormToken is never reached, and the change is // never called either (a POST attempted with no session is treated the same as a page with no session). func TestPOSTWithNoSessionDoesNotReachTheFormTokenCheckOrTheChange(t *testing.T) { fi := newFakeIssuer(t) auth := testAuth(t, fi, operatorsFile(t, "alice"), nil) var calls int handler := auth.RequireSession(RequireFormToken(changingHandler(&calls))) w := postSend(t, handler, nil, "whatever") if w.Code != http.StatusFound { t.Fatalf("status %d, want 302 (redirect to sign in)", w.Code) } if calls != 0 { t.Fatalf("the change was called %d time(s) with no session", calls) } }