# 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 --}}
```
## 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
```
## 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 --}}
@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 --}}
{{-- resources/views/livewire/register-form.blade.php --}}
```
```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.