Skip to content

Add Cloudflare Turnstile to the registration flow - #481

Draft
simonhamp wants to merge 1 commit into
mainfrom
turnstile-registration
Draft

Add Cloudflare Turnstile to the registration flow#481
simonhamp wants to merge 1 commit into
mainfrom
turnstile-registration

Conversation

@simonhamp

Copy link
Copy Markdown
Member

Why

Registration is getting hit by bot signups. This gates it behind Cloudflare Turnstile.

The project already had Turnstile plumbing — App\Rules\Turnstile, services.turnstile.* config, and a widget on the lead submission form — so this extends that rather than adding a parallel integration.

What changed

  • app/Rules/Turnstile.php — hardened per Cloudflare's siteverify guidance:

    • optional expectedAction constructor arg, so a token minted on another form can't be replayed against registration
    • 10s request timeout and a 2048-char token bound
    • opt-in hostname pinning via TURNSTILE_HOSTNAMES
    • fails closed on connection errors and non-2xx responses (previously an outage would have thrown a 500)

    Default constructor args leave the lead form's new Turnstile usage unchanged.

  • app/Http/Requests/Auth/RegisterRequest.php — new, mirroring the sibling LoginRequest. Moves the existing register rules out of the controller and adds cf-turnstile-response. The controller body is otherwise untouched.

  • resources/views/components/turnstile.blade.php — reusable component. @once-pushes api.js, renders the widget with data-theme="auto" (the site has dark mode) and data-size="flexible", and slots in the Flux error.

  • resources/views/components/layouts/auth.blade.php — added the @stack('head') the other layouts already have.

  • 12 new tests covering the happy path, missing/oversized tokens, siteverify failure, action mismatch, Cloudflare unreachable, 5xx responses, and hostname pinning both ways.

Verification

Full suite green: 1488 tests, 4016 assertions.

Also driven through a real browser at /register — the widget mounts, Cloudflare injects the hidden cf-turnstile-response input into the form, and a bad challenge redirects back with the error rendered in place. The rule was additionally exercised against the live siteverify endpoint: valid secret passes, invalid secret fails, action mismatch fails.

Before merging

  1. Set TURNSTILE_SECRET_KEY in production. It's intentionally not in this PR. Until it's set the rule short-circuits and registration is unprotected. That fail-open is the pre-existing pattern from the lead form, kept so local dev and CI don't need Cloudflare — but it does mean a missing secret silently disables the gate. Happy to make it hard-fail in production instead.
  2. Add the production domains to the widget's allowlist in the Cloudflare dashboard, alongside TURNSTILE_SITE_KEY.

Notes

  • Cloudflare's setup flow assumes a Workers backend and wrangler secret put. There's no Worker here, so that step maps onto Laravel's env store instead.
  • TURNSTILE_HOSTNAMES is opt-in rather than fail-closed-when-empty; defaulting it on would have immediately broken the existing lead form and local dev.
  • The action pin rejects tokens minted by any other form, which is the point — but it also means a stale cached /register page rendered before this deploy (without data-action) will fail until a refresh.

🤖 Generated with Claude Code

Registration was open to bot signups. This gates it behind the existing
Turnstile widget already used by the lead submission form.

Extends App\Rules\Turnstile with optional action pinning, a request
timeout, a token length bound, opt-in hostname pinning, and fail-closed
handling of connection errors and non-2xx responses. Default constructor
args leave the lead form's usage unchanged.

Register validation moves into a RegisterRequest, mirroring the sibling
LoginRequest, so the controller body stays as it was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant