Turnstile is only a widget until the Worker calls siteverify

The browser token proves nothing alone. POST it to turnstile/v0/siteverify with the secret; check success, action and hostname; tokens are single-use, 5 min.

Security

· Chapter

41

·

2

min read

The answer. Turnstile has two halves. The widget runs in the page and hands the form a token in cf-turnstile-response. The half that protects anything is server-side: the Worker POSTs that token and the site's secret to the siteverify endpoint and rejects the request unless the answer says success: true, the action matches what the page declared, and the hostname is one of yours. Each token is valid for five minutes and can be verified exactly once, so a replayed submission fails on its own. Three of our Workers (audit signup, guide delivery, beta register) run this same twelve-line check before touching D1 or sending mail.

The pattern.

async function verifyTurnstile(token: string, request: Request, env: Env): Promise<boolean> {
  const body = new URLSearchParams({
    secret: env.TURNSTILE_SECRET, response: token,
    remoteip: request.headers.get("CF-Connecting-IP") ?? "",
  });
  const r = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
    method: "POST", body, signal: AbortSignal.timeout(10_000),
  });
  if (!r.ok) return false;                                   // fail closed
  const v = await r.json<{ success: boolean; action?: string; hostname?: string }>();
  return v.success && v.action === "signup" && env.ALLOWED_HOSTS.split(",").includes(v.hostname ?? "");
}

Watch out.

  • The secret is a Worker secret (wrangler secret put TURNSTILE_SECRET), never a var; the site key is public and fine in a var.
  • A page that stays open after submit must call turnstile.reset(widgetId) before retrying; the old token is spent.
  • Test keys exist (always-pass, always-fail, always-block). Use them in staging so the widget never trains on your own QA.

Related: rate-limiting-binding-counts-per-key-in-the-worker · credential-free-edge-companion-hmac-site-tokens · queue-work-returns-202-client-polls-a-status-row