Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
Laravel Cloudflare Turnstile
https://github.com/ryangjchandler/laravel-cloudflare-turnstile
Admin
A simple Laravel package that provides helpers for setting up and validating Cloudflare Turnstile
...
Tokens:
5,483
Snippets:
28
Trust Score:
8.9
Update:
6 months ago
Context
Skills
Chat
Benchmark
84.7
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Laravel Cloudflare Turnstile Laravel Cloudflare Turnstile is a PHP package that provides seamless integration with Cloudflare's Turnstile CAPTCHA service for Laravel applications. It offers a simple, drop-in solution to protect forms from bot submissions and spam without the friction of traditional CAPTCHAs. The package handles both the client-side widget rendering and server-side validation through Laravel's validation system. The package includes Blade components for easy frontend integration, custom validation rules for backend verification, and built-in support for Livewire applications. It provides automatic retry logic for API requests, comprehensive error handling with localized messages, and supports multiple widgets on a single page. The client communicates directly with Cloudflare's API to verify tokens, ensuring secure validation of user interactions. ## Installation and Configuration Installing the package via Composer and configuring credentials ```bash # Install package composer require ryangjchandler/laravel-cloudflare-turnstile # Add to .env file TURNSTILE_SITE_KEY="1x00000000000000000000AA" TURNSTILE_SECRET_KEY="1x0000000000000000000000000000000AA" ``` ```php // config/services.php return [ 'turnstile' => [ 'key' => env('TURNSTILE_SITE_KEY'), 'secret' => env('TURNSTILE_SECRET_KEY'), ], ]; ``` ## Blade Component Integration Rendering the Turnstile widget in HTML forms ```blade {{-- resources/views/layouts/app.blade.php --}} <html> <head> @turnstileScripts() </head> <body> {{ $slot }} </body> </html> {{-- resources/views/auth/login.blade.php --}} <form action="/login" method="POST"> @csrf <input type="email" name="email" required> <input type="password" name="password" required> <x-turnstile /> @error('cf-turnstile-response') <div class="alert">{{ $message }}</div> @enderror <button type="submit">Login</button> </form> ``` ## Validation Rule - Rule Macro Using the Rule::turnstile() macro for server-side validation ```php use Illuminate\Http\Request; use Illuminate\Validation\Rule; class LoginController extends Controller { public function login(Request $request) { $validated = $request->validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string'], 'cf-turnstile-response' => ['required', Rule::turnstile()], ]); // Proceed with authentication if (Auth::attempt($validated)) { return redirect()->intended('dashboard'); } return back()->withErrors([ 'email' => 'Invalid credentials', ]); } } ``` ## Validation Rule - String Extension Using the 'turnstile' string validation rule ```php use Illuminate\Http\Request; class ContactController extends Controller { public function submit(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email', 'message' => 'required|string', 'cf-turnstile-response' => 'required|turnstile', ]); // Process contact form Mail::to('admin@example.com')->send(new ContactMessage($validated)); return back()->with('success', 'Message sent successfully!'); } } ``` ## Validation Rule - Dependency Injection Injecting the Turnstile rule object directly into controller methods ```php use Illuminate\Http\Request; use RyanChandler\LaravelCloudflareTurnstile\Rules\Turnstile; class RegisterController extends Controller { public function register(Request $request, Turnstile $turnstile) { $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users'], 'password' => ['required', 'confirmed', 'min:8'], 'cf-turnstile-response' => ['required', $turnstile], ]); $user = User::create($validated); Auth::login($user); return redirect('/dashboard'); } } ``` ## Validation Rule - Manual Instantiation Creating rule instance using app() helper ```php use Illuminate\Http\Request; use RyanChandler\LaravelCloudflareTurnstile\Rules\Turnstile; class CommentController extends Controller { public function store(Request $request, $postId) { $validated = $request->validate([ 'body' => ['required', 'string', 'max:1000'], 'cf-turnstile-response' => ['required', app(Turnstile::class)], ]); $post = Post::findOrFail($postId); $post->comments()->create([ 'user_id' => auth()->id(), 'body' => $validated['body'], ]); return redirect()->back()->with('success', 'Comment posted!'); } } ``` ## Customized Widget Configuration Configuring widget appearance and callback functions ```blade <form action="/submit" method="POST"> @csrf <x-turnstile data-theme="dark" data-size="compact" data-action="login" data-cdata="sessionid-123456789" data-callback="onTurnstileSuccess" data-expired-callback="onTurnstileExpired" data-error-callback="onTurnstileError" data-tabindex="1" /> <button type="submit">Submit</button> </form> <script> function onTurnstileSuccess(token) { console.log('Turnstile validated:', token); } function onTurnstileExpired() { console.warn('Turnstile token expired'); } function onTurnstileError(error) { console.error('Turnstile error:', error); } </script> ``` ## Livewire Integration Using Turnstile with Livewire wire:model binding ```php // app/Livewire/ContactForm.php use Livewire\Component; use Illuminate\Validation\Rule; class ContactForm extends Component { public $name = ''; public $email = ''; public $message = ''; public $captcha = null; protected function rules() { return [ 'name' => 'required|string|max:255', 'email' => 'required|email', 'message' => 'required|string', 'captcha' => ['required', Rule::turnstile()], ]; } public function submit() { $validated = $this->validate(); // Process form Mail::to('support@example.com')->send(new ContactMessage($validated)); $this->reset(['message', 'captcha']); session()->flash('success', 'Message sent!'); } public function render() { return view('livewire.contact-form'); } } ``` ```blade {{-- resources/views/livewire/contact-form.blade.php --}} <div> <form wire:submit.prevent="submit"> <input type="text" wire:model="name" placeholder="Name"> @error('name') <span class="error">{{ $message }}</span> @enderror <input type="email" wire:model="email" placeholder="Email"> @error('email') <span class="error">{{ $message }}</span> @enderror <textarea wire:model="message" placeholder="Message"></textarea> @error('message') <span class="error">{{ $message }}</span> @enderror <x-turnstile wire:model="captcha" /> @error('captcha') <span class="error">{{ $message }}</span> @enderror <button type="submit">Send Message</button> </form> @if (session()->has('success')) <div class="success">{{ session('success') }}</div> @endif </div> ``` ## Multiple Livewire Widgets Using multiple Turnstile widgets on a single page with unique IDs ```blade {{-- resources/views/dashboard.blade.php --}} <div> {{-- Login form with first widget --}} <livewire:login-form /> {{-- Registration form with second widget --}} <livewire:register-form /> </div> {{-- resources/views/livewire/login-form.blade.php --}} <form wire:submit.prevent="login"> <x-turnstile id="login_captcha" wire:model="loginCaptcha" /> @error('loginCaptcha') <span>{{ $message }}</span> @enderror <button type="submit">Login</button> </form> {{-- resources/views/livewire/register-form.blade.php --}} <form wire:submit.prevent="register"> <x-turnstile id="register_captcha" wire:model="registerCaptcha" /> @error('registerCaptcha') <span>{{ $message }}</span> @enderror <button type="submit">Register</button> </form> ``` ```php // app/Livewire/LoginForm.php class LoginForm extends Component { public $loginCaptcha = null; protected function rules() { return [ 'loginCaptcha' => ['required', 'turnstile'], ]; } } // app/Livewire/RegisterForm.php class RegisterForm extends Component { public $registerCaptcha = null; protected function rules() { return [ 'registerCaptcha' => ['required', 'turnstile'], ]; } } ``` ## TurnstileClient Direct Usage Using the TurnstileClient service directly for custom verification ```php use RyanChandler\LaravelCloudflareTurnstile\TurnstileClient; use Illuminate\Http\Request; class CustomVerificationController extends Controller { public function __construct( protected TurnstileClient $turnstile ) {} public function verify(Request $request) { $token = $request->input('cf-turnstile-response'); $response = $this->turnstile->siteverify($token); if ($response->success) { return response()->json([ 'status' => 'success', 'message' => 'Verification passed', ]); } return response()->json([ 'status' => 'error', 'message' => 'Verification failed', 'errors' => $response->errorCodes, ], 422); } } ``` ## Testing with Test Keys Using Cloudflare's test site keys and secret keys for development ```php use RyanChandler\LaravelCloudflareTurnstile\TurnstileClient; use Illuminate\Support\Facades\Config; // In your test file or seeder class TurnstileTestSetup { public static function configureForTesting($mode = 'pass') { Config::set('services.turnstile', [ 'key' => match($mode) { 'pass' => TurnstileClient::SITEKEY_ALWAYS_PASSES_VISIBLE, 'block' => TurnstileClient::SITEKEY_ALWAYS_BLOCKS_VISIBLE, 'interactive' => TurnstileClient::SITEKEY_FORCE_INTERACTIVE_VISIBLE, 'invisible-pass' => TurnstileClient::SITEKEY_ALWAYS_PASSES_INVISIBLE, 'invisible-block' => TurnstileClient::SITEKEY_ALWAYS_BLOCKS_INVISIBLE, }, 'secret' => match($mode) { 'pass', 'interactive', 'invisible-pass' => TurnstileClient::SECRET_KEY_ALWAYS_PASSES, 'block', 'invisible-block' => TurnstileClient::SECRET_KEY_ALWAYS_FAILS, 'token-spent' => TurnstileClient::SECRET_KEY_TOKEN_SPENT, }, ]); } } // Usage in tests test('login with valid captcha', function () { TurnstileTestSetup::configureForTesting('pass'); $response = $this->post('/login', [ 'email' => 'user@example.com', 'password' => 'password', 'cf-turnstile-response' => TurnstileClient::RESPONSE_DUMMY_TOKEN, ]); $response->assertRedirect('/dashboard'); }); ``` ## Error Handling and Localization Handling validation errors with custom messages ```php // resources/lang/en/cloudflare-turnstile/errors.php return [ 'missing-input-secret' => 'The secret parameter was not passed.', 'invalid-input-secret' => 'The secret parameter was invalid or did not exist.', 'missing-input-response' => 'The response parameter was not passed.', 'invalid-input-response' => 'The response parameter is invalid or has expired.', 'bad-request' => 'The request was rejected because it was malformed.', 'timeout-or-duplicate' => 'The response parameter has already been validated before.', 'internal-error' => 'An internal error happened while validating the response.', 'unexpected' => 'An unexpected error occurred.', ]; // resources/lang/es/cloudflare-turnstile/errors.php return [ 'missing-input-secret' => 'No se pasó el parámetro secreto.', 'invalid-input-secret' => 'El parámetro secreto no es válido o no existe.', 'missing-input-response' => 'No se pasó el parámetro de respuesta.', 'invalid-input-response' => 'El parámetro de respuesta no es válido o ha expirado.', 'bad-request' => 'La solicitud fue rechazada porque estaba mal formada.', 'timeout-or-duplicate' => 'El parámetro de respuesta ya se ha validado antes.', 'internal-error' => 'Ocurrió un error interno al validar la respuesta.', 'unexpected' => 'Ocurrió un error inesperado.', ]; ``` ## API Response Structure Understanding the SiteverifyResponse object structure ```php use RyanChandler\LaravelCloudflareTurnstile\TurnstileClient; $client = app(TurnstileClient::class); $response = $client->siteverify($token); // Response properties var_dump($response->success); // bool: true if validation passed var_dump($response->errorCodes); // array: error codes if validation failed // Convert to array $data = $response->toArray(); // [ // 'success' => true, // 'error-codes' => [] // ] // Failed response example // [ // 'success' => false, // 'error-codes' => ['invalid-input-response', 'timeout-or-duplicate'] // ] ``` ## Summary Laravel Cloudflare Turnstile provides a comprehensive solution for protecting Laravel applications from automated bot submissions using Cloudflare's modern CAPTCHA alternative. The package integrates seamlessly with Laravel's validation system through multiple methods: Rule macros, string-based validation rules, and dependency injection. It supports both traditional form submissions and Livewire-powered reactive components, with automatic token management and widget resets. The package handles all aspects of Turnstile integration including frontend widget rendering via Blade components, backend API communication with automatic retry logic, error handling with localized messages in multiple languages, and comprehensive testing support with Cloudflare's test keys. Whether building a simple contact form or a complex multi-step registration process with multiple widgets, the package provides the flexibility and reliability needed for production applications while maintaining a clean, Laravel-idiomatic API.