| Install | |
|---|---|
composer require danieledesantis/laravel-cloudflare-turnstile-react-blade |
|
| Latest Version: | 1.0.0 |
| PHP: | ^8.1|^8.2|^8.3 |
Cloudflare Turnstile integration for Laravel with built-in support for Blade, React, and Inertia.js. Protect your Laravel applications from bots and abuse with Cloudflare's free CAPTCHA alternative.
Read the Quick Start Guide to get up and running in 5 minutes.
composer require danieledesantis/laravel-cloudflare-turnstile-react-blade
php artisan vendor:publish --tag=turnstile-config
If you are using React or Inertia.js, publish the TypeScript components:
php artisan vendor:publish --tag=turnstile-components
This will publish the components to resources/js/Components/Turnstile/:
hooks/useTurnstile.tsx - React hook for Turnstilecomponents/Turnstile.tsx - React component for Turnstileindex.ts - ExportsYou can then import them directly in your code:
import { useTurnstile, Turnstile } from '@/Components/Turnstile';
If you are using the Blade templating engine, publish the component:
php artisan vendor:publish --tag=turnstile-views
This will publish the component to resources/views/vendor/turnstile/components/turnstile-widget.blade.php.
You can now use it in your code:
<x-turnstile />
Add your Cloudflare Turnstile credentials to your .env file:
TURNSTILE_SITEKEY=your-site-key
TURNSTILE_SECRETKEY=your-secret-key
Get your keys from Cloudflare Dashboard.
You can add Laravel validation for the Cloudflare Turnstile response using one of the following methods:
use DanieleDeSantis\LaravelTurnstile\Http\Rules\TurnstileValidation;
public function store(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
'cf-turnstile-response' => ['required', new TurnstileValidation()],
]);
// Your logic here
}
use DanieleDeSantis\LaravelTurnstile\Facades\Turnstile;
public function store(Request $request)
{
try {
Turnstile::verify(
$request->input('cf-turnstile-response'),
$request->ip()
);
// Verification successful
} catch (\Exception $e) {
return back()->withErrors(['turnstile' => $e->getMessage()]);
}
}
Protect entire routes or route groups:
use DanieleDeSantis\LaravelTurnstile\Http\Middleware\VerifyTurnstile;
// In routes/web.php
Route::post('/contact', [ContactController::class, 'store'])
->middleware('turnstile');
// With custom token field
Route::post('/register', [RegisterController::class, 'store'])
->middleware('turnstile:captcha-token');
Publish the React components using
php artisan vendor:publish --tag=turnstile-components
Pass the sitekey to your component
return Inertia::render('Auth/Login', [
'turnstile_sitekey' => config('turnstile.sitekey'),
]);
Add the widget to you form using one of the following methods:
import { Turnstile } from '@/Components/Turnstile';
import { useForm } from '@inertiajs/react';
export default function LoginForm({ turnstile_sitekey }) {
const { data, setData, post, processing, errors } = useForm({
// Your form fields
'cf-turnstile-response': '',
});
const submit = e => {
e.preventDefault();
post(route('login'));
};
return (
<form>
{/* Your form fields */}
<Turnstile
sitekey={turnstile_sitekey}
onSuccess={(token: string) => setData('cf-turnstile-response', token)}
onError={() => setData('cf-turnstile-response', '')}
onExpire={() => setData('cf-turnstile-response', '')}
theme='auto'
size='normal'
/>
<button type='submit'>Login</button>
</form>
);
}
import { useTurnstile } from '@/Components/Turnstile';
import { useForm } from '@inertiajs/react';
export default function LoginForm({ turnstile_sitekey }) {
const { data, setData, post } = useForm({
// Your form fields
'cf-turnstile-response': '',
});
const { turnstileRef } = useTurnstile({
sitekey: turnstile_sitekey,
onSuccess: (token: string) => setData('cf-turnstile-response', token),
onError: () => setData('cf-turnstile-response', ''),
onExpire: () => setData('cf-turnstile-response', ''),
});
const submit = e => {
e.preventDefault();
post(route('login'));
};
return (
<form onSubmit={submit}>
{/* Your form fields */}
<div ref={turnstileRef}></div>
<button type='submit'>Login</button>
</form>
);
}
The package includes full TypeScript support:
interface Window {
turnstile?: {
render: (element: HTMLElement, options: TurnstileOptions) => string;
reset: (widgetId: string) => void;
remove: (widgetId: string) => void;
};
}
Publish the Blade components using
php artisan vendor:publish --tag=turnstile-views
Add the component to your forms:
<form method="POST" action="/login">
@csrf
<!-- Your form fields -->
<x-turnstile />
<button type="submit">Login</button>
</form>
<x-turnstile
theme="dark"
size="compact"
field-name="captcha-token"
/>
Available options:
theme: auto, light, or dark (default: auto)size: normal, compact, or flexible (default: normal)field-name: Custom field name (default: cf-turnstile-response)callback: JavaScript callback function nameerror-callback: Error callback function nameexpired-callback: Expired callback function nameThe configuration file config/turnstile.php provides extensive customization:
return [
// Enable/disable Turnstile globally
'enabled' => env('TURNSTILE_ENABLED', true),
// Your Cloudflare credentials
'sitekey' => env('TURNSTILE_SITEKEY', ''),
'secretkey' => env('TURNSTILE_SECRETKEY', ''),
// Verification endpoint
'endpoint' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
// Request timeout in seconds
'timeout' => 10,
// Custom error messages
'error_messages' => [
'verification_failed' => 'The security verification failed. Please try again.',
'missing_token' => 'Security verification is required.',
'timeout' => 'Security verification timed out. Please try again.',
],
// Testing mode (bypasses verification)
'testing' => env('TURNSTILE_TESTING', env('APP_ENV') === 'testing'),
// Skip verification for these IPs
'skip_ips' => [],
// Widget appearance
'theme' => 'auto', // auto, light, dark
'size' => 'normal', // normal, compact, flexible
];
The package automatically bypasses Turnstile verification when APP_ENV=testing or TURNSTILE_TESTING=true.
use DanieleDeSantis\LaravelTurnstile\Facades\Turnstile;
class LoginTest extends TestCase
{
public function test_user_can_login()
{
// Turnstile is automatically disabled in testing
$response = $this->post('/login', [
'email' => 'test@example.com',
'password' => 'password',
'cf-turnstile-response' => 'test-token', // Any value works
]);
$response->assertRedirect('/dashboard');
}
public function test_turnstile_verification()
{
// Enable verification for specific tests
Turnstile::disableTesting();
// Your test logic
// Re-enable testing mode
Turnstile::enableTesting();
}
}
composer install
./vendor/bin/phpunit
use DanieleDeSantis\LaravelTurnstile\Facades\Turnstile;
// Check if Turnstile is enabled
if (Turnstile::isEnabled()) {
// Get the site key
$siteKey = Turnstile::getSiteKey();
}
// Verify a token
try {
$verified = Turnstile::verify($token, $ipAddress);
} catch (TurnstileVerificationException $e) {
// Handle verification failure
$errorCodes = $e->getErrorCodes();
}
Skip verification for trusted IPs (useful for local development):
TURNSTILE_SKIP_IPS=127.0.0.1,192.168.1.100
Or in config/turnstile.php:
'skip_ips' => ['127.0.0.1', '::1'],
Customize error messages in config/turnstile.php:
'error_messages' => [
'verification_failed' => 'Custom verification failed message',
'missing_token' => 'Custom missing token message',
'timeout' => 'Custom timeout message',
],
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class AuthenticatedSessionController extends Controller
{
public function create(): Response
{
return Inertia::render('Auth/Login', [
'turnstile_sitekey' => config('turnstile.sitekey'),
]);
}
public function store(LoginRequest $request): RedirectResponse
{
// Validation happens in LoginRequest with TurnstileValidation rule
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard'));
}
}
<?php
namespace App\Http\Requests\Auth;
use DanieleDeSantis\LaravelTurnstile\Http\Rules\TurnstileValidation;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function rules(): array
{
$rules = [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
if (config('turnstile.enabled')) {
$rules['cf-turnstile-response'] = ['required', 'string', new TurnstileValidation()];
}
return $rules;
}
}
Widget Not Displaying
.envVerification Always Fails
cf-turnstile-response)Testing Issues
Make sure TURNSTILE_TESTING=true or APP_ENV=testing is set in your test environment.
Cloudflare Turnstile may return the following error codes:
missing-input-secret - The secret parameter was not passedinvalid-input-secret - The secret parameter was invalidmissing-input-response - The response parameter was not passedinvalid-input-response - The response parameter is invalid or has expiredbad-request - The request was rejected because it was malformedtimeout-or-duplicate - The response parameter has already been validated beforeIf you discover any security-related issues, please email info@danieledesantis.net instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.
For support, please open an issue on GitHub or contact info@danieledesantis.net.
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Developed by Daniele De Santis