lambda-studio/turnstile
Laravel package for Cloudflare Turnstile captcha validation. Includes a ValidTurnstile validation rule to verify the cf-turnstile-response token in requests, plus a simple Blade form example using your configured site key and Turnstile script.
Installation
composer require lambda-studio/turnstile
Publish Config
php artisan vendor:publish --provider="LambdaStudio\Turnstile\TurnstileServiceProvider"
Update .env with:
TURNSTILE_SITE_KEY=your_site_key
TURNSTILE_SECRET_KEY=your_secret_key
First Validation
Add the rule to a form request in app/Http/Requests:
use LambdaStudio\Turnstile\Rules\ValidTurnstile;
public function rules()
{
return [
'cf-turnstile-response' => [
'required',
'string',
new ValidTurnstile,
],
];
}
Frontend Integration Include the Turnstile widget in your Blade template:
<div class="cf-turnstile" data-sitekey="{{ config('turnstile.site_key') }}"></div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
Pattern: Use the ValidTurnstile rule in form requests or controllers.
// In a controller
$request->validate([
'cf-turnstile-response' => [new ValidTurnstile],
]);
Pro Tip: Extend the rule for custom logic:
use LambdaStudio\Turnstile\Rules\ValidTurnstile as BaseValidTurnstile;
class CustomValidTurnstile extends BaseValidTurnstile
{
public function passes($attribute, $value)
{
// Add custom logic (e.g., IP whitelisting)
return parent::passes($attribute, $value);
}
}
Pattern: Apply ValidateTurnstile middleware to routes or globally.
// In app/Http/Kernel.php
protected $middleware = [
\LambdaStudio\Turnstile\Http\Middleware\ValidateTurnstile::class,
];
Use Case: Protect admin routes or high-risk endpoints (e.g., password resets).
Customization: Override the middleware to exclude specific routes:
public function handle($request, Closure $next)
{
if ($request->is('api/*')) {
return $next($request);
}
return parent::handle($request, $next);
}
Pattern: Use the facade for direct API calls (e.g., in services).
use LambdaStudio\Turnstile\Facades\Turnstile;
public function verifyToken($token)
{
return Turnstile::verify($token);
}
Use Case: Validate tokens outside form requests (e.g., API endpoints).
Pattern: Catch TurnstileException for graceful failures.
try {
Turnstile::verify($token);
} catch (\LambdaStudio\Turnstile\Exceptions\TurnstileException $e) {
return response()->json(['error' => 'CAPTCHA verification failed'], 400);
}
Custom Messages:
Override translations in resources/lang/en/validation.php:
'turnstile' => [
'invalid' => 'The CAPTCHA verification failed. Please try again.',
],
ValidTurnstile in form requests for clean separation.@stack('scripts')
@push('scripts')
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
@endpush
use LambdaStudio\Turnstile\Facades\Turnstile;
public function testValidation()
{
Turnstile::shouldReceive('verify')
->once()
->andReturn(true);
$response = $this->post('/submit', ['cf-turnstile-response' => 'valid_token']);
$response->assertRedirect('/success');
}
Missing Frontend Script
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
Key Mismatch
site_key/secret_key mismatch causes silent failures.Route::get('/test-turnstile', function () {
return Turnstile::verify('invalid_token'); // Should return false
});
Null Values
'required' to the rule:
'cf-turnstile-response' => ['required', new ValidTurnstile],
Middleware Overhead
Route::middleware(['turnstile'])->group(function () {
Route::post('/contact');
});
PHP Version
Rate Limiting
Enable Logging
Add to config/turnstile.php:
'debug' => env('TURNSTILE_DEBUG', false),
Logs will appear in storage/logs/laravel.log.
Raw API Responses Use the service directly to inspect responses:
$response = Turnstile::service()->verify($token);
dd($response->getBody()->getContents());
Common Errors
invalid: Token failed Cloudflare’s validation.missing-input-secret: secret_key is missing or incorrect.timeout-or-duplicate: Token expired or was already used.Custom Validation Logic
Extend the ValidTurnstile rule:
class CustomValidTurnstile extends \LambdaStudio\Turnstile\Rules\ValidTurnstile
{
public function passes($attribute, $value)
{
if (str_contains($value, 'test')) {
return false; // Block test tokens
}
return parent::passes($attribute, $value);
}
}
Blade Macros (Todo) Implement manually until the package adds them:
use Illuminate\Support\Facades\Blade;
Blade::directive('turnstile', function ($siteKey) {
return <<<EOT
<div class="cf-turnstile" data-sitekey="$siteKey"></div>
@push('scripts')
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
@endpush
EOT;
});
Usage:
@turnstile(config('turnstile.site_key'))
Service Extensions Add custom methods to the service:
namespace App\Services;
use LambdaStudio\Turnstile\TurnstileService;
class ExtendedTurnstileService extends TurnstileService
{
public function verifyWithRetry($token, $retries = 3)
{
for ($i = 0; $i < $retries; $i++) {
try {
return parent::verify($token);
} catch (\Exception $e) {
if ($i === $retries - 1) throw $e;
sleep(1);
}
}
}
}
Event Listeners Listen for validation events (not built-in; use Laravel’s events):
use Illuminate\Support\Facades\Event;
use Illuminate\Validation\Events\ValidationFailed;
Event::
How can I help you explore Laravel packages today?