auth_test.go
| 1 | package server |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "net/url" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | // postWith is postForm carrying cookies, which /logout needs: it is a guarded |
| 12 | // route, so without a session it never reaches the handler. |
| 13 | func postWith(router http.Handler, path string, cookies []*http.Cookie) *httptest.ResponseRecorder { |
| 14 | req := httptest.NewRequest("POST", path, nil) |
| 15 | for _, c := range cookies { |
| 16 | req.AddCookie(c) |
| 17 | } |
| 18 | w := httptest.NewRecorder() |
| 19 | router.ServeHTTP(w, req) |
| 20 | return w |
| 21 | } |
| 22 | |
| 23 | // sessionCookie returns the session cookie set on a response. A cleared cookie |
| 24 | // carries an empty value and does not count as one. |
| 25 | func sessionCookie(cookies []*http.Cookie) *http.Cookie { |
| 26 | for _, c := range cookies { |
| 27 | if c.Name == "session" && c.Value != "" { |
| 28 | return c |
| 29 | } |
| 30 | } |
| 31 | return nil |
| 32 | } |
| 33 | |
| 34 | func TestAuthGuardsRoutes(t *testing.T) { |
| 35 | srv, _, cleanup := setupTestServer(t) |
| 36 | defer cleanup() |
| 37 | // Past the test wrapper that signs every request in: these requests must |
| 38 | // arrive without a session. |
| 39 | router := srv.Server.Router() |
| 40 | |
| 41 | for _, path := range []string{"/", "/library", "/queue", "/settings", "/subscriptions", "/download"} { |
| 42 | got := getWith(router, path, nil) |
| 43 | if got.Code != http.StatusSeeOther || got.Header().Get("Location") != "/login" { |
| 44 | t.Errorf("%s without a session = %d %q, want 303 /login", path, got.Code, got.Header().Get("Location")) |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | // Mutating routes must be guarded too, not just the pages that link to them. |
| 49 | if got := postForm(router, "/queue/clear", nil); got.Header().Get("Location") != "/login" { |
| 50 | t.Errorf("clearing the queue without a session redirected to %q, want /login", got.Header().Get("Location")) |
| 51 | } |
| 52 | |
| 53 | login := getWith(router, "/login", nil) |
| 54 | if login.Code != http.StatusOK { |
| 55 | t.Errorf("login page = %d, want 200", login.Code) |
| 56 | } |
| 57 | // Nothing the navigation points at is reachable without a session, so the |
| 58 | // login page shows none of it. |
| 59 | for _, link := range []string{`href="/library"`, `href="/queue"`, `href="/settings"`, "/logout"} { |
| 60 | if strings.Contains(login.Body.String(), link) { |
| 61 | t.Errorf("login page shows %s", link) |
| 62 | } |
| 63 | } |
| 64 | if got := getWith(router, "/healthz", nil); got.Code == http.StatusSeeOther { |
| 65 | t.Error("health check redirected to login") |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | func TestLoginRejectsWrongCredentials(t *testing.T) { |
| 70 | srv, _, cleanup := setupTestServer(t) |
| 71 | defer cleanup() |
| 72 | router := srv.Server.Router() |
| 73 | |
| 74 | for _, tc := range []struct { |
| 75 | name, user, pass string |
| 76 | }{ |
| 77 | {"wrong password", testUsername, "not-the-password"}, |
| 78 | {"wrong user", "intruder", testPassword}, |
| 79 | {"empty password", testUsername, ""}, |
| 80 | // The stored value is a hash, so posting it must not be accepted as if it |
| 81 | // were the password itself. |
| 82 | {"the hash itself", testUsername, testPasswordHash}, |
| 83 | } { |
| 84 | w := postForm(router, "/login", url.Values{"username": {tc.user}, "password": {tc.pass}}) |
| 85 | if c := sessionCookie(w.Result().Cookies()); c != nil { |
| 86 | t.Errorf("%s issued a session", tc.name) |
| 87 | } |
| 88 | if k, _, ok := flash(w); !ok || k != "error" { |
| 89 | t.Errorf("%s did not flash an error", tc.name) |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestLoginGrantsAccess(t *testing.T) { |
| 95 | srv, _, cleanup := setupTestServer(t) |
| 96 | defer cleanup() |
| 97 | router := srv.Server.Router() |
| 98 | |
| 99 | w := postForm(router, "/login", url.Values{ |
| 100 | "username": {testUsername}, |
| 101 | "password": {testPassword}, |
| 102 | }) |
| 103 | session := sessionCookie(w.Result().Cookies()) |
| 104 | if session == nil { |
| 105 | t.Fatal("correct credentials issued no session") |
| 106 | } |
| 107 | if !session.HttpOnly { |
| 108 | t.Error("session cookie is not HttpOnly") |
| 109 | } |
| 110 | if got := getWith(router, "/library", []*http.Cookie{session}); got.Code != http.StatusOK { |
| 111 | t.Errorf("library with a session = %d, want 200", got.Code) |
| 112 | } |
| 113 | if got := getWith(router, "/login", []*http.Cookie{session}); got.Code != http.StatusSeeOther { |
| 114 | t.Errorf("login page with a session = %d, want 303", got.Code) |
| 115 | } |
| 116 | |
| 117 | // The cookie is signed, so an edited expiry must not extend the session. |
| 118 | forged := &http.Cookie{Name: "session", Value: strings.Replace(session.Value, "|", "9|", 1)} |
| 119 | if got := getWith(router, "/library", []*http.Cookie{forged}); got.Code != http.StatusSeeOther { |
| 120 | t.Errorf("library with a forged session = %d, want 303", got.Code) |
| 121 | } |
| 122 | |
| 123 | out := postWith(router, "/logout", []*http.Cookie{session}) |
| 124 | if sessionCookie(out.Result().Cookies()) != nil { |
| 125 | t.Error("logout left a session cookie behind") |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | func TestLogoutRevokesTheCookie(t *testing.T) { |
| 130 | srv, _, cleanup := setupTestServer(t) |
| 131 | defer cleanup() |
| 132 | router := srv.Server.Router() |
| 133 | |
| 134 | in := postForm(router, "/login", url.Values{ |
| 135 | "username": {testUsername}, |
| 136 | "password": {testPassword}, |
| 137 | }) |
| 138 | session := sessionCookie(in.Result().Cookies()) |
| 139 | if session == nil { |
| 140 | t.Fatal("login issued no session") |
| 141 | } |
| 142 | |
| 143 | postWith(router, "/logout", []*http.Cookie{session}) |
| 144 | |
| 145 | // Clearing the browser's cookie is not enough: a copy taken before the |
| 146 | // sign-out must stop working too. |
| 147 | if got := getWith(router, "/library", []*http.Cookie{session}); got.Code != http.StatusSeeOther { |
| 148 | t.Errorf("a captured cookie still works after logout: %d, want 303", got.Code) |
| 149 | } |
| 150 | } |
| 151 |