133 lines
4.6 KiB
Go
133 lines
4.6 KiB
Go
package main
|
|
|
|
// 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)
|
|
}
|
|
}
|