eolica / laravel-cloudflare-turnstile
Cloudflare Turnstile integration for Laravel
Package info
github.com/Eolica-Web/php-cloudflare-turnstile-laravel
pkg:composer/eolica/laravel-cloudflare-turnstile
Requires
- php: ^8.5
- eolica/cloudflare-turnstile: v0.4.0
- guzzlehttp/psr7: ^2.4 || ^3.0
- illuminate/config: ^13.0
- illuminate/container: ^13.0
- illuminate/contracts: ^13.0
- illuminate/http: ^13.0
- illuminate/support: ^13.0
- illuminate/validation: ^13.0
- illuminate/view: ^13.0
- psr/http-client: ^1.0
- psr/http-message: ^2.0
Requires (Dev)
None
Suggests
- livewire/livewire: Resets the widget in Livewire components after its token is verified
- seophp/head-laravel: Adds the Turnstile script to the <head> managed by seophp/head with TurnstilePlugin
Provides
None
Conflicts
None
Replaces
None
README
Laravel integration of eolica/cloudflare-turnstile.
Requirements
- PHP 8.5 or higher
- Laravel 13
- Livewire 3 or 4, optional, for the Livewire integration
Installation
Install via Composer:
composer require eolica/laravel-cloudflare-turnstile
The service provider is registered automatically.
This package always requires the same version of eolica/cloudflare-turnstile, so update both together with --with-dependencies:
composer update eolica/laravel-cloudflare-turnstile --with-dependencies
# Or, to a version outside your current constraint:
composer require eolica/laravel-cloudflare-turnstile:^0.3 --with-dependencies
Configuration
Add your site key and secret key to the .env file:
TURNSTILE_SITE_KEY=your-site-key TURNSTILE_SECRET_KEY=your-secret-key
Optionally, set the hostname the tokens must be issued for:
TURNSTILE_HOSTNAME=example.com
To customize the configuration, publish the configuration file:
php artisan vendor:publish --tag=turnstile-config
Rendering the widget
Include Cloudflare's script once in your layout, for example in the <head>:
<head> <x-turnstile::script /> </head>
It also adds a preconnect hint so the browser connects to Cloudflare early. Any attribute, such as a nonce, is added to the <script> tag. Include it in the layout itself, not inside a Livewire component: the scripts Livewire adds to the page when it updates a component don't run.
Then add the widget inside your form:
<form method="POST" action="/contact"> @csrf <x-turnstile::widget /> <button type="submit">Send</button> </form>
The widget adds the token to the form in the cf-turnstile-response field.
Configure the widget with any of these attributes:
<x-turnstile::widget action="contact" cdata="campaign-42" theme="dark" size="flexible" language="es" appearance="interaction-only" />
See Cloudflare's widget configuration for the values each one accepts. Any other attribute, such as class or data-callback, is added to the widget's container.
The widget is shown in your application's locale unless you set a language, and in English if Cloudflare doesn't support that language. Use language="auto" to show it in the visitor's browser language instead.
Livewire
Bind the widget to a property of your component with wire:model:
<form wire:submit="send"> <x-turnstile::widget wire:model="captcha" /> <button type="submit">Send</button> </form>
use Eolica\LaravelCloudflareTurnstile\Rules\Turnstile; use Livewire\Component; class Contact extends Component { public string $email = ''; public ?string $captcha = null; public function send(): void { $this->validate([ 'email' => ['required', 'email'], 'captcha' => [new Turnstile()], ]); } }
The widget works with wire:navigate and in components rendered after the page loads. It also works with form objects. If the page doesn't include Cloudflare's script, the widget logs an error in the browser's console.
A token can only be verified once, so the widget is reset after Cloudflare verifies its token, whether it passes or fails. The property returns to its initial value and the visitor gets a new token. You don't need to do anything for this.
If another field fails, the token isn't verified, so the visitor keeps it and can submit the form again without solving a new challenge.
seophp/head
If your application uses seophp/head-laravel, add Cloudflare's script and the preconnect hint to the <head> it renders with the TurnstilePlugin, instead of the script component:
use Eolica\LaravelCloudflareTurnstile\Head\TurnstilePlugin; protected function configure(Head $head): void { $head->plugins([ TurnstilePlugin::make(), ]); }
Validation
Validate the token the widget adds to your form with the Turnstile rule:
use Eolica\LaravelCloudflareTurnstile\Rules\Turnstile; $request->validate([ 'cf-turnstile-response' => [new Turnstile()], ]);
The token is only checked once every other field passes, wherever the rule is placed. When another field fails, the rule doesn't send the token to Cloudflare and doesn't add an error, as the validation fails anyway. This avoids a request and keeps the token unused.
The rule fails when the token is missing, so it doesn't need the required rule.
It also sends the visitor's IP address to Cloudflare, taken from $request->ip(). If your application runs behind a proxy, such as Cloudflare itself or a load balancer, configure the trusted proxies so the IP address is the visitor's and not the proxy's. To stop sending it:
TURNSTILE_SEND_REMOTE_IP=false
Or override the configuration for a single rule:
new Turnstile(sendRemoteIp: false)
Pass the action configured in the widget to check that the token was issued for it:
new Turnstile(action: 'login')
If Cloudflare can't be reached, the validation fails and the exception is reported to your exception handler.
To customize the error message or translate it, publish the translations:
php artisan vendor:publish --tag=turnstile-translations
Or set a custom message for a single form with the turnstile key, as with any other rule:
$request->validate( ['cf-turnstile-response' => [new Turnstile()]], ['cf-turnstile-response.turnstile' => 'Please confirm you are human.'], );
Events
The rule dispatches a TokenVerified event with Cloudflare's response after each verification, whether it passes or fails. The visitor only sees a generic error, so listen to it to find out why a token was rejected:
use Eolica\LaravelCloudflareTurnstile\Events\TokenVerified; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; Event::listen(function (TokenVerified $event): void { if (! $event->response->success) { Log::debug('Turnstile rejected a token.', [ 'action' => $event->action, 'errors' => $event->response->errorCodes, ]); } });
$event->response is the VerifyResponse returned by the client and $event->action the action passed to the rule. The event is not dispatched when the field is missing or isn't a string, when another field fails, or when Cloudflare can't be reached and the exception is reported instead.
Using the client directly
Eolica\CloudflareTurnstile\TurnstileClient is registered in the container, configured with your secret key and expected hostname. Inject it wherever you need it:
use Eolica\CloudflareTurnstile\TurnstileClient; public function store(Request $request, TurnstileClient $turnstile) { $response = $turnstile ->withExpectedAction('login') ->verify($request->input('cf-turnstile-response', ''), remoteIp: $request->ip()); if (!$response->success) { // Reject the request } }
See the core package documentation for everything TurnstileClient offers.
HTTP client
Requests to Cloudflare are sent through Laravel's HTTP client, so they fire its events, show up in tools like Telescope or Nightwatch, and use its global configuration. For example, to set a timeout for every outgoing request:
use Illuminate\Support\Facades\Http; Http::globalOptions(['timeout' => 5]);
Testing
Fake Cloudflare's responses with Http::fake():
use Eolica\CloudflareTurnstile\TurnstileClient; use Illuminate\Support\Facades\Http; Http::fake([ TurnstileClient::SITEVERIFY_URL => Http::response(['success' => true, 'action' => 'contact']), ]);
The rule compares the response with the expected values, so include the action you pass to the rule, and the hostname if you set TURNSTILE_HOSTNAME. Otherwise the rule fails.
Http::preventStrayRequests() also prevents real requests to Cloudflare.
To try the widget locally, use Cloudflare's testing keys, such as the site key 1x00000000000000000000AA and the secret key 1x0000000000000000000000000000000AA, which always pass. The hostname and the action are not checked with the test secret keys, so a rule with an action passes too.
License
The MIT License (MIT). See the license file for more information.