D18 — Cloudflare Turnstile on unauthenticated endpoints (RESOLVED)
Architecture decision record, relocated verbatim from the retired single-file
docs/tenancy/decision log. Status, thematic clusters, and how to record a new ADR: the decision log index.
App\Http\Middleware\VerifyTurnstile (alias turnstile) verifies a Turnstile token
server-side against Cloudflare's siteverify API, applied to the two unauthenticated,
abuse-prone endpoints: POST /api/v1/login (credential stuffing) and
POST /api/v1/tenant-discovery (email enumeration). The SPA renders the widget (site_key)
and submits cf-turnstile-response; failures return 422 on that field. config/turnstile.php
(enabled, site_key, secret_key, verify_url) is env-driven. Non-prod uses Cloudflare's
public test keys (always-pass: site 1x00000000000000000000AA / secret
1x0000000000000000000000000000000AA) with TURNSTILE_ENABLED=true, so dev exercises the real
widget + siteverify flow with no Cloudflare account (verified end-to-end: no token → 422, dummy
token → 200). The test suite forces it off (tests/bootstrap.php); TurnstileTest opts in
with Http::fake(). Production sets real keys via 1Password → SSM.