The directory role answers the front page to anonymous visitors
oficina.works is about to run the directory role, whose "/" was gated behind sessions.Require. That would have replaced the front page with a sign-in redirect: every stranger who typed the domain would meet a form with no explanation of what they were signing in to. "/" now reads the session rather than requiring it. No session gets the front page; a session gets the directory. An expired cookie degrades to the front page instead of a redirect loop. The two tests that covered this asserted the mechanism (a 303 to /auth/start) alongside the property (the catalogue never reaches an anonymous request). The property is what matters and is now checked directly, against the response body: an anonymous request must contain the front page and must not contain a team chooser, an /apps/ link or the Docs entry path. Same for the credential-cookie separation in exchange_test.go, which asserted 303 for the same reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 files changed,
+39
−5
internal/home/directory.go+14 −1internal/home/directory_catalog_test.go+16 −2internal/home/exchange_test.go+9 −2
diff --git a/internal/home/directory.go b/internal/home/directory.go| index b9b04a1..7b38318 100644 |
| --- a/internal/home/directory.go |
| +++ b/internal/home/directory.go |
| @@ -80,7 +80,19 @@ func (a *App) directoryRouter() http.Handler { |
| r.Get("/signin", func(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, handoff.StartPath, http.StatusSeeOther) |
| }) |
| - r.With(a.sessions.Require).Get("/", func(w http.ResponseWriter, r *http.Request) { |
| + // One URL, two audiences. A visitor with no session gets the front |
| + // page — what Oficina is, the apps, and the way in. A signed-in |
| + // person gets their directory. Gating this behind sessions.Require |
| + // instead, as it was, sends every stranger who types oficina.works |
| + // to a sign-in form: a door with no building attached to it. |
| + // |
| + // The session is READ here, never required, so an expired cookie |
| + // degrades to the front page rather than to a redirect loop. |
| + r.Get("/", func(w http.ResponseWriter, r *http.Request) { |
| + if _, ok := a.sessions.From(r); !ok { |
| + a.handleIndex(w, r) |
| + return |
| + } |
| w.Header().Set("Cache-Control", "no-store") |
| a.render(w, 200, "directory.html", map[string]any{"Apps": directoryCatalog(*a.runtime)}) |
| }) |
| @@ -96,6 +108,7 @@ func (a *App) directoryRouter() http.Handler { |
| r.Get("/static/tokens.css", serveAsset(staticAsset("tokens.css"), "text/css; charset=utf-8")) |
| r.Get("/static/theme.css", serveAsset(theme.CSS(), "text/css; charset=utf-8")) |
| mountBrand(r) |
| + mountSite(r) |
| }) |
| return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| if req.URL.Path == rosterPath { |
diff --git a/internal/home/directory_catalog_test.go b/internal/home/directory_catalog_test.go| index 7727bc8..02ed273 100644 |
| --- a/internal/home/directory_catalog_test.go |
| +++ b/internal/home/directory_catalog_test.go |
| @@ -7,9 +7,23 @@ import ( |
| func TestDirectoryCatalogRequiresSigninAndDoesNotInventLaunchDestinations(t *testing.T) { |
| f := newMountedChooser(t) |
| + // "/" answers the front page to a visitor with no session, and the |
| + // catalogue only to a session that exists. The property under test is |
| + // unchanged and is the second condition: the catalogue — team choosers, |
| + // per-app entry paths — must never reach an anonymous request. What |
| + // changed is the mechanism. This used to 303 to /auth/start, which sent |
| + // every stranger who typed oficina.works to a sign-in form; it now |
| + // renders the front page, so the assertion is on CONTENT rather than on |
| + // a status code, which is the thing that actually matters. |
| anonymous := f.request("GET", "/", "", "", nil) |
| - if anonymous.Code != 303 || !strings.HasPrefix(anonymous.Header().Get("Location"), "/auth/start") || strings.Contains(anonymous.Body.String(), "Choose a team") { |
| - t.Fatal("anonymous directory exposed catalog", anonymous.Code, anonymous.Header()) |
| + anonymousBody := anonymous.Body.String() |
| + if anonymous.Code != 200 || !strings.Contains(anonymousBody, "office suite") { |
| + t.Fatal("anonymous directory did not serve the front page", anonymous.Code, anonymous.Header()) |
| + } |
| + for _, leak := range []string{"Choose a team", `href="/apps/`, docsEntryPath} { |
| + if strings.Contains(anonymousBody, leak) { |
| + t.Fatal("anonymous front page exposed catalogue", leak) |
| + } |
| } |
| cookie := f.session(t, chooserPerson) |
| page := f.request("GET", "/", "", "", cookie) |
diff --git a/internal/home/exchange_test.go b/internal/home/exchange_test.go| index d87aee9..f20cf79 100644 |
| --- a/internal/home/exchange_test.go |
| +++ b/internal/home/exchange_test.go |
| @@ -164,8 +164,15 @@ func TestHomeDirectoryExchangeAndPrivacy(t *testing.T) { |
| if subject == "" || strings.Contains(subject, "@") || authTime != old.Format(time.RFC3339) { |
| t.Fatal("identity or freshness changed") |
| } |
| - if rec := exchangeRequest(p.directory, "GET", directoryOrigin+"/", "", nil, credentialSession); rec.Code != 303 { |
| - t.Fatal("credential cookie opened directory") |
| + // A cookie minted on the credential origin must not carry a directory |
| + // session. "/" no longer redirects an unauthenticated caller — it |
| + // renders the front page — so the check is that the credential cookie |
| + // gets that anonymous page and none of the signed-in directory, which |
| + // asserts the same separation against the response body instead of |
| + // against a redirect that is no longer how the route reports it. |
| + if rec := exchangeRequest(p.directory, "GET", directoryOrigin+"/", "", nil, credentialSession); rec.Code != 200 || |
| + strings.Contains(rec.Body.String(), "You are signed in.") || strings.Contains(rec.Body.String(), "Choose a team") { |
| + t.Fatal("credential cookie opened directory", rec.Code) |
| } |
| if rec := exchangeRequest(p.credential.App, "GET", Origin+"/authorize/resume", "", nil, intent, directorySession); rec.Code != 303 { |
| t.Fatal("directory cookie opened credentials") |