Cloudflare Turnstile is free privacy-friendly replacement for CAPTCHAs. Turnstile stops abuse and assures visitors are real persons.
Compared to Google ReCaptcha, Cloudflare does not share data collected with advertisers.
In some cases bots can beat Google Captcha but they're failing on Turnstile.
Setup for Cloudflare Turnstile
As in every Laravel tutorial I'll use Laravel Breeze starter kit in a Blade-flavor.
The most important form on every site is a sign-up form, that's why I decided to secure register form with Cloudflare Turnstile.
First thing I need to obtain site kyes.
Cloudflare Turnstile obtaining site keys
Go to your Cloudflare Dashboard and from the sidebar choose Turnstile.
Choose to add new site. You can name your Turnstile integration using some friendly name. In the domain input you can select 1 or more domains.
Regarding Widget Mode the default one Managed is good.
After clicking the Create button Cloudflare will display keys.
Add site keys to Laravel
Inside config/services.php
add configuration:
'turnstile' => [
'key' => env('TURNSTILE_SITE_KEY'),
'secret' => env('TURNSTILE_SECRET_KEY')
]
And add respective keys in the .env
file:
TURNSTILE_SITE_KEY=""
TURNSTILE_SECRET_KEY=""
Laravel Turnstile Client
I like to keep my custom logic inside app/Logic
folder in the dedicated namespace, so I'll create Turnstile/TurnstileClient.php
inside.
<?php
namespace App\Logic\Turnstile;
use Illuminate\Support\Facades\Http;
class TurnstileClient
{
public function check(string $response): CheckResponse
{
$response = Http::retry(3, 100)
->asForm()
->acceptJson()
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => config('services.turnstile.secret'),
'response' => $response,
]);
if (! $response->ok()) {
return new CheckResponse(success: false, errorCodes: []);
}
return new CheckResponse(success: $response->json('success'), errorCodes: $response->json('error-codes'));
}
}
This class has a check
method which passes response and a secret key to the Cloudflare Turnstile API.
Based on the response it'll return CheckResponse
object with or without error codes.
CheckResponse class is very simple:
<?php
namespace App\Logic\Turnstile;
use Illuminate\Contracts\Support\Arrayable;
class CheckResponse implements Arrayable
{
public function __construct(
public readonly bool $success,
public readonly array $errorCodes,
) {
}
public function toArray(): array
{
return [
'success' => $this->success,
'error-codes' => $this->errorCodes,
];
}
}
With this our Turnstile client is ready and we can implement it inside custom Laravel validation rule.
Turnstile Custom Laravel Validation Rule
In this tutorial I'll implement validation rule in Register logic but it can be implemented into any other validation flow you may have.
Create a rule:
php artisan make:rule Turnstile
And add Turnstile client call inside:
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use App\Logic\Turnstile\TurnstileClient;
class Turnstile implements Rule
{
protected array $messages = [];
public function __construct(
) {
}
public function passes($attribute, $value)
{
$client = new TurnstileClient();
$response = $client->check($value);
if ($response->success) {
return true;
}
foreach ($response->errorCodes as $errorCode) {
$this->messages[] = $this->mapErrorCodeToMessage($errorCode);
}
return false;
}
public function message()
{
return $this->messages;
}
protected function mapErrorCodeToMessage(string $code): string
{
return match ($code) {
'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.',
default => 'An unexpected error occurred.',
};
}
}
Integrate Turnstile Rule into Register logic
Go visit Auth\RegisteredUserController
and the validate array add:
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'cf-turnstile-response' => ['required', new Turnstile()]
]);
Now on every submit Laravel will call Turnstile validation rule.
The only thing left is to display it on the front end.
Implementing Turnstile into Form
In head section add JS script:
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
And in the form under password input add Turnstile section:
<div class="cf-turnstile" data-sitekey="{{ config('services.turnstile.key') }}"></div>
That's it.
Now Cloudflare Turnstile will be rendered on the Register page:
More resources for Laravel Sign up pages
If you're looking how to implement social OAuth logins into your Laravel app see Laravel OAuth Authentication.
If you want magic login links see Login into Laravel application with a Magic link.
After implementing this tutorial if you're not happy with Cloudflare Turnstile go read our guide on Google ReCaptcha.