# 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 --}} @turnstileScripts() {{ $slot }} {{-- resources/views/auth/login.blade.php --}}
@csrf @error('cf-turnstile-response')
{{ $message }}
@enderror ``` ## 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
@csrf ``` ## 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 --}}
@error('name') {{ $message }} @enderror @error('email') {{ $message }} @enderror @error('message') {{ $message }} @enderror @error('captcha') {{ $message }} @enderror @if (session()->has('success'))
{{ session('success') }}
@endif
``` ## Multiple Livewire Widgets Using multiple Turnstile widgets on a single page with unique IDs ```blade {{-- resources/views/dashboard.blade.php --}}
{{-- Login form with first widget --}} {{-- Registration form with second widget --}}
{{-- resources/views/livewire/login-form.blade.php --}}
@error('loginCaptcha') {{ $message }} @enderror {{-- resources/views/livewire/register-form.blade.php --}}
@error('registerCaptcha') {{ $message }} @enderror ``` ```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.