ryangjchandler/laravel-cloudflare-turnstile
Installation
composer require ryangjchandler/laravel-cloudflare-turnstile
Publish the config file:
php artisan vendor:publish --provider="RyanChandler\CloudflareTurnstile\CloudflareTurnstileServiceProvider" --tag="config"
Configure Cloudflare Turnstile
Add your Turnstile site key and secret to config/services.php:
'cloudflare-turnstile' => [
'site_key' => env('CLOUDFLARE_TURNSTILE_SITE_KEY'),
'secret_key' => env('CLOUDFLARE_TURNSTILE_SECRET_KEY'),
],
Set these in your .env:
CLOUDFLARE_TURNSTILE_SITE_KEY=your_site_key
CLOUDFLARE_TURNSTILE_SECRET_KEY=your_secret_key
First Use Case: Form Validation Add the validation rule to a form request or controller:
use RyanChandler\CloudflareTurnstile\Rules\Turnstile;
public function rules()
{
return [
'turnstile_token' => ['required', new Turnstile],
];
}
Display the Turnstile Widget In your Blade template:
@turnstile(['sitekey' => config('services.cloudflare-turnstile.site_key')])
Request Validation
Use the Turnstile rule in form requests or controllers:
public function rules()
{
return [
'turnstile_token' => ['required', new Turnstile],
];
}
Optionally, pass a custom error message:
'turnstile_token' => ['required', new Turnstile('Invalid Turnstile token.')],
Handling Errors Customize error messages in your language files:
'validation.turnstile' => 'The Turnstile verification failed.',
Manual Verification Verify tokens programmatically (e.g., in API endpoints):
use RyanChandler\CloudflareTurnstile\Facades\CloudflareTurnstile;
$isValid = CloudflareTurnstile::verify($request->input('turnstile_token'));
if (!$isValid) {
return response()->json(['error' => 'Invalid Turnstile token'], 422);
}
Async Verification For non-critical paths, use async verification with a queue job:
CloudflareTurnstile::verifyAsync($token)->onQueue('verifications');
Environment-Specific Keys Use different keys per environment (e.g., staging vs. production):
'cloudflare-turnstile' => [
'site_key' => env('CLOUDFLARE_TURNSTILE_SITE_KEY_' . app()->environment()),
'secret_key' => env('CLOUDFLARE_TURNSTILE_SECRET_KEY_' . app()->environment()),
],
Multi-Tenant Keys Override keys per tenant using middleware or service providers:
CloudflareTurnstile::setSiteKey($tenant->turnstile_site_key);
Reusable Widgets Create a custom Blade component for consistency:
@component('components.turnstile', ['sitekey' => config('services.cloudflare-turnstile.site_key')])
@endcomponent
Dynamic Attributes
Extend the @turnstile directive to include custom data attributes:
@turnstile(['sitekey' => $siteKey, 'data-theme' => 'dark'])
Mocking Verification
Use the CloudflareTurnstile facade in tests:
CloudflareTurnstile::shouldReceive('verify')->once()->andReturn(true);
Or stub the HTTP client:
$this->mock(Http::class)->shouldReceive('post')->andReturn(Http::response(['success' => true]));
Test Helpers Create a trait for common test scenarios:
trait TurnstileTests
{
protected function assertTurnstileValidation($token, $shouldPass = true)
{
$validator = Validator::make(['turnstile_token' => $token], ['turnstile_token' => [new Turnstile]]);
$this->assertEquals($shouldPass, $validator->passes());
}
}
Missing .env Variables
Ensure CLOUDFLARE_TURNSTILE_SITE_KEY and CLOUDFLARE_TURNSTILE_SECRET_KEY are set. The package won’t throw errors if these are missing, but validation will fail silently.
Fix: Add validation in AppServiceProvider:
if (empty(config('services.cloudflare-turnstile.site_key'))) {
throw new \RuntimeException('Cloudflare Turnstile site key is not configured.');
}
Incorrect Config Path
The package expects keys under config('services.cloudflare-turnstile'). If you customize the config path, update the service provider:
$this->app->singleton('cloudflare-turnstile', function ($app) {
return new CloudflareTurnstile($app['config']['custom.cloudflare.turnstile']);
});
Silent Failures
Turnstile validation errors may not bubble up as expected. Use ->sometimes() or custom messages:
'turnstile_token' => ['required', 'sometimes', new Turnstile('Please complete the Turnstile challenge.')],
Rate Limiting Cloudflare may throttle requests. Implement retry logic:
try {
$isValid = CloudflareTurnstile::verify($token);
} catch (\GuzzleHttp\Exception\RequestException $e) {
if ($e->getCode() === 429) {
retry()->times(3)->later(2)->try(fn() => CloudflareTurnstile::verify($token));
}
throw $e;
}
HTTP Client Errors If Turnstile API calls fail, verify:
secret_key is correct.https://challenges.cloudflare.com/turnstile/v0/siteverify) is accessible.$client = Http::withOptions(['debug' => function ($config) {
return array_merge($config, ['debug' => true]);
}]);
IP Restrictions Ensure your server’s IP isn’t blocked by Cloudflare. Test with:
curl -X POST "https://challenges.cloudflare.com/turnstile/v0/siteverify" \
-d "secret=$SECRET_KEY" \
-d "response=$TOKEN"
Directive Overrides
If @turnstile conflicts with other packages, rename it in the service provider:
Blade::directive('cfTurnstile', function ($expression) {
return "<?php echo \\RyanChandler\\CloudflareTurnstile\\Blade::turnstile($expression); ?>";
});
Then use @cfTurnstile(['sitekey' => $key]) in Blade.
Escaping Issues If the widget HTML isn’t rendering, ensure no extra quotes or syntax errors exist in the directive call.
Async Verification Overhead Async verification adds queue processing time. Use it only for non-critical paths (e.g., newsletter signups). Alternative: Cache verification results for short-lived tokens (not recommended for security-sensitive paths).
Token Expiry Turnstile tokens expire quickly (typically 2 minutes). Avoid pre-verifying tokens before form submission.
Turnstile class:
use RyanChandler\CloudflareTurnstile\Rules\Turnstile as BaseTurnstile;
class CustomTurnstile extends BaseTurnstile
{
public function passed($attribute, $value)
{
$isValid = parent::passed($attribute, $value);
// Add custom logic (e.g., check against a whitelist)
How can I help you explore Laravel packages today?