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

The signup page renders the Turnstile widget when a site key is set, with CSP widened on /signup only, and the admin settings and README say how to turn it on #1723

Merged opened this

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

Merged

scout/forge:scout/1666-the-signup-page-renders-the-turnstile-wi → main 3 commits fork 5 threads · 5 unresolved

  • Merged. scout/1666-the-signup-page-renders-the-turnstile-wi was merged into main as 665bbe4.

Review

Sign in to review this change.
3 files changed +40 −1
src/web/pages/secrets.ts +9 −1
@@ -145,6 +145,13 @@ function requireAdmin(ctx: Ctx): Response | undefined {
145145 return undefined;
146146 }
147147
148 /** One quiet line: whether signup asks Turnstile, and the two variables that decide it. */

Good: a constant string that only checks whether the secret is set. It reads no value and shows only to admins.

149 function humanCheckLine(ctx: Ctx): string {
150 return ctx.app.config.turnstileSecret === undefined
151 ? "Signup has no human check. Set FORGE_TURNSTILE_SECRET and FORGE_TURNSTILE_SITEKEY at boot to turn it on."
152 : "Signup asks Cloudflare Turnstile (FORGE_TURNSTILE_SECRET and FORGE_TURNSTILE_SITEKEY).";

This on branch has no test. Add an admin GET of /settings/secrets on a forge booted with a Turnstile secret and site key. Assert this wording is there, the off wording is not, and the secret value never appears.

153 }
154
148155 // GET /settings/secrets
149156 export function secretsPage(ctx: Ctx): Response {
150157 const denied = requireAdmin(ctx);
@@ -155,7 +162,8 @@ export function secretsPage(ctx: Ctx): Response {
155162 body: html`<div class="page-head"><h1>Secrets</h1></div>
156163 ${settingsLayout(
157164 accountSections("secrets", { admin: true }),
158 secretsSection(ctx, "forge", "")!,
165 html`${secretsSection(ctx, "forge", "")!}
166 <p class="muted small">${humanCheckLine(ctx)}</p>`,
159167 "Account settings",
160168 )}`,
161169 });
test/secret-store.test.ts +3
@@ -320,6 +320,9 @@ describe("forge-level secrets", () => {
320320 expect(await (await get("/settings/secrets", cookie)).text()).toContain(
321321 "CF_TOKEN",
322322 );
323 expect(await (await get("/settings/secrets", cookie)).text()).toContain(

This covers off only. Add the matching on case (see the review body) so both branches of humanCheckLine fail a test if they break.

324 "Signup has no human check. Set FORGE_TURNSTILE_SECRET and FORGE_TURNSTILE_SITEKEY",
325 );
323326 const bob = await login("bob");
324327 expect((await get("/settings/secrets", bob)).status).toBe(404);
325328 expect(await (await get("/acme", bob)).text()).not.toContain("Seal");
test/turnstile.test.ts +28
@@ -159,6 +159,34 @@ describe("turnstile doors", () => {
159159 expect(csp).toMatch(new RegExp(`${directive}[^;]*https://challenges.cloudflare.com`));
160160 });
161161
162 test("only /signup widens the policy, and the secret reaches no page", async () => {
163 const { base } = await boot({ turnstileSecret: "SECRET-VALUE", turnstileSiteKey: "0xSITE" });
164 for (const path of ["/", "/login", "/explore"]) {
165 const response = await fetch(`${base}${path}`);
166 expect(response.headers.get("content-security-policy")).not.toContain("cloudflare");
167 expect(await response.text()).not.toContain("SECRET-VALUE");
168 }
169 expect(await (await fetch(`${base}/signup`)).text()).not.toContain("SECRET-VALUE");
170 });
171
172 test("the admin settings line says on when Turnstile is on, and never the secret", async () => {
173 const { base } = await boot({ turnstileSecret: "SECRET-VALUE", turnstileSiteKey: "0xSITE" });
174 expect((await signup(base, "good")).status).toBe(201);
175 const signedIn = await fetch(`${base}/signin`, {
176 method: "POST",
177 redirect: "manual",
178 headers: { "content-type": "application/x-www-form-urlencoded", origin: base },
179 body: new URLSearchParams({ username: "casey", password: "correct horse battery" }),
180 });
181 const cookie = (signedIn.headers.get("set-cookie") ?? "").split(";")[0] ?? "";
182 const page = await fetch(`${base}/settings/secrets`, { headers: { cookie } });
183 expect(page.status).toBe(200);
184 const text = await page.text();
185 expect(text).toContain("Signup asks Cloudflare Turnstile");
186 expect(text).not.toContain("Signup has no human check");
187 expect(text).not.toContain("SECRET-VALUE");
188 });
189
162190 test("off, the page has no widget and the policy is unchanged", async () => {
163191 const { base } = await boot();
164192 const response = await fetch(`${base}/signup`);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs