coderflex/laravel-turnstile
Add Cloudflare Turnstile CAPTCHA to Laravel with minimal setup. Includes config publishing, env-based site/secret keys, validation integration, and customizable/translatable error messages for protecting forms and endpoints from bots.
composer require coderflex/laravel-turnstile
php artisan vendor:publish --tag="turnstile-config"
.env:
TURNSTILE_SITE_KEY=your_site_key
TURNSTILE_SECRET_KEY=your_secret_key
<x-turnstile-widget />
<form method="POST" action="/contact">
@csrf
<input type="text" name="name" required>
<x-turnstile-widget theme="auto" />
<button type="submit">Send</button>
</form>
// In your controller
use Coderflex\LaravelTurnstile\Facades\LaravelTurnstile;
public function store(Request $request) {
$validated = $request->validate([
'name' => 'required|string',
'cf-turnstile-response' => ['required', new \Coderflex\LaravelTurnstile\Rules\TurnstileCheck()]
]);
// Proceed if validation passes
}
theme="auto" for system preference detection or hardcode theme="dark" for consistency:
<x-turnstile-widget theme="dark" language="{{ app()->getLocale() }}" />
@if(request()->is('register') || request()->is('contact'))
<x-turnstile-widget />
@endif
<x-turnstile-widget callback="handleTurnstileSuccess" errorCallback="handleTurnstileError" />
function handleTurnstileSuccess(token) {
document.getElementById('turnstile-token').value = token;
}
$request->validate([
'cf-turnstile-response' => [new TurnstileCheck(), 'required'],
]);
$response = LaravelTurnstile::validate($request->input('cf-turnstile-response'));
if (!$response['success']) {
return back()->withErrors(['captcha' => trans('turnstile::turnstile.error')]);
}
$responses = ['step1' => $request->input('step1_captcha'), 'step2' => $request->input('step2_captcha')];
foreach ($responses as $token) {
if (!LaravelTurnstile::validate($token)['success']) {
return back()->withErrors(['captcha' => 'Invalid CAPTCHA']);
}
}
public function verifyCaptcha(Request $request) {
$response = LaravelTurnstile::validate($request->input('token'));
return response()->json($response);
}
ValidateTurnstileJob::dispatch($request->input('cf-turnstile-response'))
->delay(now()->addMinutes(5));
public function test_validation_fails_with_invalid_token() {
$rule = new TurnstileCheck();
$this->assertFalse($rule->passes('data', 'invalid_token'));
}
public function test_widget_renders_correctly() {
$view = view('vendor.turnstile::widget', ['theme' => 'dark']);
$view->assertSee('data-theme="dark"');
}
.env.testing:
TURNSTILE_SITE_KEY=0x4AF50000ABCDEFCD
TURNSTILE_SECRET_KEY=0x4AF50000ABCDEFCD
Missing CSRF Token:
@csrf in Blade forms. Omitting it may cause silent validation failures.@csrf in forms with the widget.Case-Sensitive Field Names:
cf-turnstile-response (case-sensitive). Using CF-TURNSTILE-RESPONSE or cfTurnstileResponse will fail validation.cf-turnstile-response in your form fields.Secret Key Exposure:
TURNSTILE_SECRET_KEY in .env is not masked by default in Laravel’s .env files. Ensure it’s not committed to version control..env to .gitignore and use environment variables in CI/CD.Rate Limiting:
429 Too Many Requests.JavaScript Dependency:
Enable Debug Logging:
Add to config/turnstile.php:
'debug' => env('TURNSTILE_DEBUG', false),
Then check storage/logs/laravel.log for validation details.
Inspect API Responses:
Cloudflare returns detailed errors in the error-codes field. Log the full response:
$response = LaravelTurnstile::validate($token);
\Log::debug('Turnstile Response:', $response);
Test with Dummy Keys: Use Cloudflare’s test keys to verify integration before going live:
TURNSTILE_SITE_KEY=0x4AF50000ABCDEFCD
TURNSTILE_SECRET_KEY=0x4AF50000ABCDEFCD
Custom Error Messages:
Override the default message in config/turnstile.php:
'error_messages' => [
'turnstile_check_message' => 'Please complete the security verification.',
],
Or translate it in your language files:
'validation' => [
'attributes' => [
'cf-turnstile-response' => 'security check',
],
'custom' => [
'cf-turnstile-response' => [
'turnstile' => 'The :attribute verification failed. Please try again.',
],
],
],
Custom Validation Logic:
Extend the TurnstileCheck rule for additional rules (e.g., IP-based allowlists):
use Coderflex\LaravelTurnstile\Rules\TurnstileCheck;
class ExtendedTurnstileCheck extends TurnstileCheck {
public function passes($attribute, $value) {
if ($this->isAllowedIP(request()->ip())) {
return true;
}
return parent::passes($attribute, $value);
}
protected function isAllowedIP($ip) {
// Your logic here
}
}
Widget Customization:
Publish the views and modify resources/views/vendor/turnstile/widget.blade.php to:
data-testid="turnstile" for testing).Event Listeners: Listen for validation events to log attempts or trigger analytics:
use Coderflex\LaravelTurnstile\Events\TurnstileValidated;
TurnstileValidated::listen(function ($event) {
\Log::info('Turnstile validation', $event->response);
});
$cacheKey
How can I help you explore Laravel packages today?