Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Turnstile Laravel Package

romanzipp/laravel-turnstile

Laravel package for integrating Cloudflare Turnstile into your forms. Includes validation rules, middleware, and a Blade component to protect against bots and spam with a privacy-friendly captcha alternative to reCAPTCHA.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require romanzipp/laravel-turnstile
php artisan vendor:publish --provider="romanzipp\Turnstile\Providers\TurnstileServiceProvider"

Auto-discovery registers the provider; the published config/turnstile.php is the first file to read — it exposes enabled, site_key, secret_key, and the HTTP client binding.

Configure keys

TURNSTILE_ENABLED=true
TURNSTILE_SITE_KEY=0x4AAAAAAA...
TURNSTILE_SECRET_KEY=0x4AAAAAAA...

For local/tests, use Cloudflare's published dummy keys (1x00000000000000000000AA / 1x0000000000000000000000000000000AA always pass) instead of disabling the feature, so you exercise the real code path.

First use case: protect a public form

Blade:

<form method="POST" action="{{ route('contact.store') }}">
    @csrf
    <x-turnstile::widget />

    <button type="submit">Send</button>
</form>

Controller — the widget injects a cf-turnstile-response field, validated with the package rule:

use romanzipp\Turnstile\Rules\Turnstile;

$request->validate([
    'cf-turnstile-response' => ['required', new Turnstile()],
]);

If that passes, the token was verified server-side against Cloudflare. Nothing else is required for the basic flow.

Implementation Patterns

Route-level protection. The package ships a middleware for protecting whole endpoints (login, register, password reset). Alias it in app/Http/Kernel.php (Laravel ≤10) or bootstrap/app.php (Laravel 11+) and attach it to the POST route — not the GET route that renders the form, since there is no token on the initial page load:

Route::post('/login', [LoginController::class, 'store'])
    ->middleware('turnstile');

Manual verification outside form requests. For APIs, Inertia, or Livewire, resolve the Turnstile service from the container and pass the token explicitly rather than relying on the request key:

use romanzipp\Turnstile\Turnstile;

if (! app(Turnstile::class)->verify($request->input('turnstile_token'))) {
    abort(422, 'Captcha verification failed.');
}

This is the pattern to use when the token arrives nested in JSON, in a custom field name, or after a Livewire round-trip.

Livewire. Render the widget inside the component and wire:ignore its container so Livewire's DOM diffing does not destroy Cloudflare's iframe. Expose the token as a public property via the widget's callback, then reset the widget after every submit attempt — including successful ones, since the token is single-use.

Inertia/Vue/React. Don't rely on the Blade component. Load https://challenges.cloudflare.com/turnstile/v0/api.js yourself, render the widget with turnstile.render(), and post the callback token in your form payload. Keep the server-side check identical to the Laravel-validated path so the rule stays in one place.

Testing. Set TURNSTILE_ENABLED=false in phpunit.xml only if you accept that the bypass branch is exercised instead of the real rule. Otherwise use the always-pass dummy secret and Http::fake() the siteverify endpoint to assert request/response handling — this catches regressions like a missing token field.

Customization. Widget options (theme, size, appearance, language) are passed through as Blade attributes; wrapper components around <x-turnstile::widget /> are the cleanest way to standardize styling across forms.

Gotchas and Tips

  • Tokens are single-use and expire (~5 minutes). The biggest day-to-day pitfall: a form fails on some other field, the user resubmits, and Turnstile fails because the token was already consumed or aged out. Always reset/re-render the widget when returning validation errors.
  • Client-side widget is not security. Only the server-side verify() call matters; never trust a hidden field the browser could forge.
  • TURNSTILE_ENABLED=false disables verification entirely. It's convenient locally, but guard it: ensure the env var can't be flipped in production, and add a test asserting the rule is active when enabled.
  • Config caching. After changing keys, run php artisan config:clear; a cached config with the old secret produces opaque "invalid input" errors from Cloudflare.
  • CSP. Sites with a strict Content-Security-Policy must allow https://challenges.cloudflare.com for script-src, frame-src, and connect-src, or the widget silently fails to render.
  • Hostname and action checks. If you configure an expected hostname or action in the Cloudflare dashboard, verify them server-side too; otherwise a token minted on another site/action is still accepted by a bare verify().
  • Debugging failures. The rule's generic message hides Cloudflare error codes (invalid-input-secret, timeout-or-duplicate, invalid-input-response). Log the raw siteverify response in non-production or wrap the rule to surface the code.
  • Proxies and hostnames. Behind a load balancer, make sure Laravel's trusted proxies / URL detection is correct; a mismatch between the request host and the configured hostname causes verification failures.
  • Turnstile is not a substitute for rate limiting. Keep throttling on login and contact endpoints — a solved captcha still allows repeated submissions.
  • Extension points. The config's HTTP client binding is the seam for swapping in a mocked or tuned client; the published config and the rule class are the only places you should need to touch to adapt behavior.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource