| 1 | package docs |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | // TestHomeSignoutChainsToIssuer: in Home mode, Docs' sign-out ends the Docs |
| 10 | // session and then hands the same POST on to {issuer}/signout, so the id.* |
| 11 | // session ends too and the next "Sign in" asks again (spec shared-chrome |
| 12 | // §9.5; the same chain connect.Routes.Signout gives every other app). |
| 13 | // Ending only the Docs session left "Sign in" on the signed-out page |
| 14 | // passing silently through the live id.* session. |
| 15 | // |
| 16 | // 307, not 303, so the hop stays a POST: id.* ignores a GET sign-out. The |
| 17 | // browser sends "Origin: null" on the redirected POST, so id.* admits it by |
| 18 | // Referer, which strict-origin-when-cross-origin makes exactly this origin. |
| 19 | func TestHomeSignoutChainsToIssuer(t *testing.T) { |
| 20 | f := newRouterHomeFixture(t) |
| 21 | b := f.browser() |
| 22 | b.localSignin(t, "alice@example.com") |
| 23 | cookie := b.cookies[f.sessions.CookieName()] |
| 24 | if cookie == nil { |
| 25 | t.Fatal("local sign-in set no session") |
| 26 | } |
| 27 | rec := b.do("POST", "/signout", "") |
| 28 | if rec.Code != http.StatusTemporaryRedirect || rec.Header().Get("Location") != bridgeIssuer+"/signout" { |
| 29 | t.Fatalf("POST /signout: status %d Location %q, want 307 to %s/signout", rec.Code, rec.Header().Get("Location"), bridgeIssuer) |
| 30 | } |
| 31 | if got := rec.Header().Get("Referrer-Policy"); got != "strict-origin-when-cross-origin" { |
| 32 | t.Fatalf("Referrer-Policy %q: id.* admits the null-Origin hop by Referer", got) |
| 33 | } |
| 34 | if got := rec.Header().Get("Cache-Control"); got != "no-store" { |
| 35 | t.Fatalf("Cache-Control %q, want no-store", got) |
| 36 | } |
| 37 | if b.cookies[f.sessions.CookieName()] != nil { |
| 38 | t.Fatal("sign-out kept the Docs session cookie") |
| 39 | } |
| 40 | old := httptest.NewRequest("GET", bridgeOrigin+"/", nil) |
| 41 | old.AddCookie(cookie) |
| 42 | oldRec := httptest.NewRecorder() |
| 43 | f.app.Handler().ServeHTTP(oldRec, old) |
| 44 | if oldRec.Code != http.StatusSeeOther { |
| 45 | t.Fatalf("revoked session still served / (status %d)", oldRec.Code) |
| 46 | } |
| 47 | // id.* returns the browser to /signed-out, which must still render and |
| 48 | // must not start another exchange by itself. |
| 49 | signedOut := b.do("GET", "/signed-out", "") |
| 50 | if signedOut.Code != http.StatusOK || signedOut.Header().Get("Location") != "" { |
| 51 | t.Fatalf("GET /signed-out: status %d Location %q", signedOut.Code, signedOut.Header().Get("Location")) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | // signoutChained asserts the Home-mode sign-out response: a 307 to the |
| 56 | // issuer's /signout. The browser then returns to /signed-out. |
| 57 | func signoutChained(t *testing.T, rec *httptest.ResponseRecorder) { |
| 58 | t.Helper() |
| 59 | if rec.Code != http.StatusTemporaryRedirect || rec.Header().Get("Location") != bridgeIssuer+"/signout" { |
| 60 | t.Fatalf("POST /signout: status %d Location %q, want 307 to %s/signout", rec.Code, rec.Header().Get("Location"), bridgeIssuer) |
| 61 | } |
| 62 | } |
| 63 | |