Public: anyone can read this; the crew can change it.

A signed-out 404 offers sign-in back to the same address, and /go/:code's dead-code 404 stays one page #4183

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4139-a-signed-out-404-offers-sign-in-back-to → main 5 commits fork 1 thread · 1 unresolved

  • Merged. scout/4139-a-signed-out-404-offers-sign-in-back-to was merged into main as c351c8b.

Review

Sign in to review this change.
4 files changed +22 −12
src/http/server.ts +4 −2
@@ -960,7 +960,7 @@ export function buildRouter(): Router<Handler> {
960960 ctx.app.config.dataDir,
961961 ctx.params.code ?? "",
962962 );
963 if (target === undefined) return notFoundPage(ctx);
963 if (target === undefined) return notFoundPage(ctx, { plain: true });

No test covers this. The dead-code test in test/web-origins.test.ts removes next= before comparing, so it passes with or without plain. Assert that the signed-out dead-code page has "Go home" and no /signin?next=, or remove plain here.

964964 return new Response(null, {
965965 status: 303,
966966 headers: { location: target, "cache-control": "no-store" },
@@ -2459,7 +2459,9 @@ async function answerGet(
24592459 auth: { user: undefined, scope: undefined, via: "none" },
24602460 theme: isThemeName(themeCookie) ? themeCookie : undefined,
24612461 };
2462 return notFoundPage(anonCtx);
2462 // No session was read, so it cannot tell signed-out from signed-in:
2463 // it offers no sign-in rather than one to someone who has signed in.
2464 return notFoundPage(anonCtx, { plain: true });
24632465 }
24642466 return noRoute(request.method, url.pathname);
24652467 }
src/web/chrome.ts +8 −9
@@ -66,13 +66,12 @@ export function crumbsFor(repo: Repo): { label: string; href: string }[] {
6666 }
6767
6868 /** The one 404 in the product: same words for "no such thing" and "not
69 * yours to see". A signed-out GET also gets the way back in, to this very
70 * address; it is offered whether or not the thing exists, so it tells nothing. */
71 export function notFoundPage(ctx: Ctx): Response {
72 const back =
73 ctx.auth.user === undefined && ctx.request.method === "GET"
74 ? btn("Sign in", { href: signInHref(ctx) })
75 : undefined;
69 * yours to see". With no session, on a GET, it also offers sign-in back to
70 * this address; `plain` leaves that out for a miss that must reveal nothing
71 * (a dead `/go/:code`). */
72 export function notFoundPage(ctx: Ctx, opts: { plain?: boolean } = {}): Response {
73 const offerSignIn =
74 !opts.plain && ctx.auth.user === undefined && ctx.request.method === "GET";
7675 return page(
7776 {
7877 ctx,
@@ -82,8 +81,8 @@ export function notFoundPage(ctx: Ctx): Response {
8281 icon: "search",
8382 title: "Nothing here",
8483 note: "The page you asked for does not exist, or is not visible to you.",
85 action: back
86 ? html`${btn("Go home", { href: "/", variant: "primary" })} ${back}`
84 action: offerSignIn
85 ? btn("Sign in", { href: signInHref(ctx), variant: "primary" })
8786 : btn("Go home", { href: "/", variant: "primary" }),
8887 })}`,
8988 },
test/web-origins.test.ts +8 −1
@@ -599,7 +599,14 @@ describe("origins on the web", () => {
599599 .replaceAll(/nonce="[^"]*"/g, 'nonce="N"')
600600 .replaceAll(/here\?path=[^"&\s]*/g, "here?path=")
601601 .replaceAll(/next=[^"]*/g, "next=");
602 expect(sameBut(await stale.text())).toBe(sameBut(await unknown.text()));
602 const staleText = await stale.text();
603 const unknownText = await unknown.text();
604 expect(sameBut(staleText)).toBe(sameBut(unknownText));
605 // A dead code offers no sign-in back to itself: it reveals nothing.
606 for (const text of [staleText, unknownText]) {
607 expect(text).toContain("Go home");
608 expect(text).not.toContain("/signin?next=");
609 }
603610
604611 // A finished flow's link dies with it, and the code is not reissued.
605612 const fresh = await asUser(carol, "/settings/origins/oauth", {
test/web-ui.test.ts +2
@@ -311,6 +311,7 @@ describe("web ui", () => {
311311 expect(out).toContain(
312312 `href="/signin?next=${encodeURIComponent(asked).replaceAll("&", "&amp;")}"`,
313313 );
314 expect(out).not.toContain("Go home");
314315 const hidden = await (await fetch(`${base}/kim/secret`)).text();
315316 const absent = await (await fetch(`${base}/kim/nothing`)).text();
316317 const scrub = (t: string) =>
@@ -322,6 +323,7 @@ describe("web ui", () => {
322323 ).text();
323324 expect(signedIn).toContain("Nothing here");
324325 expect(signedIn).not.toContain("/signin?next=");
326 expect(signedIn).toContain("Go home");
325327 });
326328
327329 test("safeNext refuses another origin", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs