Laravel: Stopping bots with a Turnstile interstitial, for free
No challenge, no site for you!
One thing I’ve seen on many sites is the usage of Cloudflare’s WAF service to protect their web application. The main feature is their Browser Integrity Check, and odds are you have found yourself in it a couple of times on your favorite web sites with a nice message about checking if your connection is secure.
This feature is included for free, but it’s only suited for small and medium sites that don’t require especial configuration. The disadvantage is using Cloudflare DNS resolvers, which on some applications it would be non-negotiable.
Worry not, with Cloudflare Turnstile we can make a similar in-app check, which can also be edited by you. Of course, it won’t handle a candle compared to the full-fledged Cloudflare WAF, but in some cases it can be enough.
Installing Turnstile in Laravel
The first thing to do is to install Laragear Turnstile, a package that I created for the purpose of integrating Cloud Turnstile challenges into Laravel applications, which is a good way to say goodbye to Google reCATPCHA now that the service is no longer free. It comes with a middleware you can apply globally.
Fire up the Terminal and tell composer to require it into your application.
composer require laragear/turnstile
The package doesn’t require your Turnstile keys on development, as it will use Cloudflare’s provided testing keys by default. On production, these will be disabled to avoid mistakenly using them on the wild Internet, so you will require your own.
Once the installation is done, we should go inside our bootstrap/app.php
file and set the turnstile.insterstitial
middleware in your web
group middleware.
Double-ensure you’re setting the middleware into the web
group. If you set this middleware globally, even your API endpoints will require Turnstile and will disrupt all connected applications that play with JSON responses.
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('web', 'turnstile.interstitial');
})
->create();
If you have an App\Http\Kernel
class in your (old) application, you can do that on the $middlewareGroup
property, under the web
key.
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// Add the middleware by its class
\Laragear\Turnstile\Http\Middleware\InterstitialMiddleware::class,
],
// ...
];
Once done, your application will redirect first-time visitors to an interstitial challenge where the Cloudflare Turnstile box will appear. This is just an static route, by default at https://yourapp.com/turnstile/interstitial
. Don’t worry, users who already completed the challenge won’t be able to see this route again.
When the challenge is successful, users will be automatically redirected to their intended route, or your home page, which is great to avoid disrupting their navigation. The library comes with a default view you can customize as you see fit.
Of course, there is much more to it. For example, you may add it to only selected group of routes instead of globally, especially if you have a place in your app that’s available publicly and is hit hard for AI web scrappers.
use Illuminate\Support\Facades\Route;
Route::prefix('photos')
->middleware('turnstile.interstitial')
->group(function () {
// ...
});
You can also disable it for authenticated users, especially if you have set your authentication routes to work with Turnstile to avoid bots logging in as humans, something that has started to become really normal thanks to the auge of AI Agents.
use Illuminate\Support\Facades\Route;
Route::prefix('public')
->middleware('turnstile.interstitial:auth')
->group(function () {
// ...
});
If you want to check it out, the full documentation is online, so give it a go: