laragear/turnstile

Use Cloudflare's no-CAPTCHA alternative in your Laravel application.

v1.4.1 2025-03-30 18:07 UTC

README

Latest Version on Packagist Latest stable test run Codecov coverage Maintainability Sonarcloud Status Laravel Octane Compatibility

Use Cloudflare's no-CAPTCHA alternative in your Laravel application.

use Illuminate\Support\Facades\Route;

Route::post('login', function () {
    // ...
})->middleware('turnstile');

Become a sponsor

Your support allows me to keep this package free, up-to-date and maintainable. Alternatively, you can spread the word on social media!.

Requirements

  • Laravel 11 or later

Installation

You can install the package via Composer:

composer require laragear/turnstile

Setup

This library comes already with the official demonstration keys to start developing your application with Cloudflare Turnstile immediately.

Once in production, you will require real keys, both of them obtainable through your Cloudflare Dashboard, and set as environment variables:

TURNSTILE_SITE_KEY=...
TURNSTILE_SECRET_KEY=...

Frontend integration

This library comes with two Blade Components to easy your development pain: <x-turnstile::script /> and <x-turnstile::widget />.

Script

You can use the <x-turnstile::script /> Blade Component to implement the Cloudflare Turnstile script in your <head> tag of your HTML view.

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- ... -->
    <title>My application</title>
    
    <x-turnstile::script />
</head>
<body>
  ...
</body>
</html>

The script will render a <script> tag using async and defer by default:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" defer async></script>

If you don't want to use async or defer, you can set any of these to false.

<x-turnstile::script :async="false" :defer="false" />
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>

You may also set explicit to true to make widgets be rendered only explicitly by your frontend JavaScript.

<x-turnstile::script :explicit="true" />
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer async></script>

Finally, you can also set a custom callback name to be executed once the script is completely loaded in your frontend, especially if you're using explicit rendering, with the onload attribute.

<x-turnstile::script :explicit="true" onload="renderAllWidgets" />
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=renderAllWidgets" defer async></script>

Site Key on JavaScript frontend

If you put the script on the <head> part of your HTML view, you may set the meta attribute to render a <meta> tag alongside the script. This tag will contain your Turnstile site key so your JavaScript frontend can retrieve use it to render the widget.

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- ... -->
    <title>My application</title>
    
    <x-turnstile::script meta />
</head>
<body>
  ...
</body>
</html>

It will render the following HTML:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" defer async></script>
<meta name="turnstile-sitekey" content="1x00000000000000000000AA" />

You will be able to retrieve the site key through Javascript by querying the meta tag with the turnstile-sitekey name.

<script setup>
import VueTurnstile from 'some-vue-turnstile library'

const siteKey = document.querySelector('meta[name="turnstile-sitekey"]').content;

const token = ref('')

// ...
</script>

<template>
    <VueTurnstile :sitekey="siteKey" v-model="token" />
    
    <!-- ... -->
</template>

Alternatively, you may set a custom name for the tag by setting a value to the meta attribute.

<x-turnstile::script meta="my-custom-key" />
<meta name="my-custom-key" content="1x00000000000000000000AA" />

Widget

Important

Remember that the Widget Mode is controlled via your Cloudflare Dashboard, not here. On development, this is controlled with testing keys.

You can use the <x-turnstile::widget /> Blade Component to add the Turnstile Widget in your forms. Depending on the Widget Mode, the Widget may render as usual or be invisible at Turnstile discretion.

<form id='login' method="POST">
    @csrf
    <input type="email" name="email">
    <input type="password" name="password">
    
    <x-turnstile::widget />
    
    <button type="submit">
        Login
    </button>
</form>

You can pass HTML attributes and data attributes to change the widget behavior. For example, you can use the data-action to differentiate multiple widgets in your application, or data-error-callback to execute a JavaScript function in your frontend if the challenge fails.

<x-turnstile::widget class="shadow-lg" data-action="auth-login" data-error-callback="tryAgain" />
<div
    class="cf-turnstile shadow-lg"
    data-sitekey="..."
    data-action="auth-login"
    data-error-callback="tryAgain"
></div>

Tip

Classes are automatically appended, so you shouldn't worry about overwriting the cf-turnstile class used by the Widget to render.

Backend integration

When issuing a form, you have three alternatives to ensure the Turnstile challenge is valid and successful, from the easiest to the more flexible:

Warning

All methods will fail on server-side errors:

  • The Cloudflare Turnstile servers are unreachable.
  • The request to Cloudflare Turnstile servers is malformed.
  • The token is duplicated or had a timeout.

Connection problems will always throw an exception.

Validating with Request

The easiest and least intrusive way to check the Turnstile Challenge is to use the Laragear\Turnstile\Http\Requests\TurnstileRequest instance in your controller. This is great if you only have a few controllers where you want to stop bots.

use App\Models\Comment;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Requests\TurnstileRequest;

Route::post('comment', function (TurnstileRequest $request) {
    $request->validate([
        'body' => 'required|string'
    ]);
    
    return Comment::create($request->only('body'));
})

You can have access to the Cloudflare Turnstile Challenge object through the challenge() method, plus additional helpers for the Challenge instance itself. For example, you may use it to double-check if the action is equal to something you expect.

use App\Models\Comment;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Requests\TurnstileRequest;

Route::post('comment', function (TurnstileRequest $request) {
    $request->validate([
        'body' => 'required|string'
    ]);
    
    if ($request->isAction('comment:store')) {
        return back()->withErrors('Invalid action');
    }
    
    return Comment::create($request->only('body'));
})

Important

The Request will check for the cf-turnstile-response key by default, plus a successful Challenge. If you need more fine-tuning, you may extend the form request class. Alternatively, you may also use a middleware, rule, or validate manually.

Extending the Form Request

If you need to create a form request and also validate the Turnstile Challenge, you may safely extend the TurnstileRequest instead of the base FormRequest. The class runs the validation before your form request authorization and rules to avoid running side effects.

namespace App\Http\Requests;

use Laragear\Turnstile\Http\Requests\TurnstileRequest;

class CommentStoreRequest extends TurnstileRequest
{
    public function rules(): array
    {
        return [
            'body' => 'required|string'
        ];
    }
}

This means your controller can safely retrieve the validated data using $request->validated(), as the token won't be considered part of the Request itself.

use App\Models\Comment;
use Illuminate\Support\Facades\Route;
use App\Http\Requests\CommentStoreRequest;

Route::post('comment', function (CommentStoreRequest $request) {
    return Comment::create($request->validated());
})

Custom key and rules

You may also edit the key and the rules where to find and check the Response Token in the request. For the case of rules, ensure you're using the turnstile rule.

namespace App\Http\Requests;

use Laragear\Turnstile\Http\Requests\TurnstileRequest;

class CommentStoreRequest extends TurnstileRequest
{
    public function getTurnstileKey()
    {
        return '_cf-custom-key';
    }
    
    protected function getTurnstileRules()
    {
        // Skip if the user is authenticated.
        return 'turnstile:auth'
    }
    
    // ...
}

Precognitive request

The TurnstileRequest won't check for the Challenge Token on Precognitive Requests, which is useful to not disrupt live-validation.

If you require custom validation on precognitive requests, you may override the skipChallengeWhenPrecognitive() method.

namespace App\Http\Requests;

use Laragear\Turnstile\Http\Requests\TurnstileRequest;

class CommentStoreRequest extends TurnstileRequest
{
    protected function skipChallengeWhenPrecognitive(): bool
    {
        return $this->hasHeader('Requires-CF-Turnstile-Challenge');
    }
    
    // ...
}

Extending the Form Request

If you need to create a form request and also validate the Turnstile Challenge, you may safely extend the TurnstileRequest instead. The class runs the validation before your form request authorization and rules.

namespace App\Http\Requests;

use Laragear\Turnstile\Http\Requests\TurnstileRequest;

class CommentStoreRequest extends TurnstileRequest
{
    public function rules(): array
    {
        return [
            'body' => 'required|string'
        ];
    }
}

This means your controller can safely retrieve the validated data using $request->validated().

use App\Models\Comment;
use Illuminate\Support\Facades\Route;
use App\Http\Requests\CommentStoreRequest;

Route::post('comment', function (CommentStoreRequest $request) {
    return Comment::create($request->validated());
})

Validating with Middleware

The turnstile middleware is a great way to check if a form submission contains a successful challenge. Simply add the middleware to the route (or group of routes) that receive the form submission, like a POST, PUT or PATCH.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('comment', function (Request $request) {
    // ...
})->middleware('turnstile');

Note

Is not suggested to use the middleware on GET methods or similar. Some browsers (or extensions) may cache or inspect ahead document links.

If you want to configure the middleware behaviour, you should use the TurnstileMiddleware class and the static helper methods.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;

Route::post('comment', function (Request $request) {
    // ...
})->middleware(TurnstileMiddleware::acceptFailed())

Custom challenge key

The middleware will check for the cf-turnstile-response key set in the form or JSON, by default. If you have edited your frontend to use another key, use the input() method of the middleware class with the key name.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;

Route::post('comment', function (Request $request) {
    // ...
})->middleware(TurnstileMiddleware::input('my-response-token-key'))

Middleware bypass when authenticated

You can configure the authentication guards to bypass the challenge requirement if the user is authenticated through the auth() method.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;

Route::post('comment', function (Request $request) {
    // ...
})->middleware(TurnstileMiddleware::auth());

By default, it will check the default authentication guard of your application. You may set specific guards by just naming them.

use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;

TurnstileMiddleware::auth('admin');

To complement this, you should add the widget to your forms only if user is a guest for the given guards.

<form id='login' method="POST">
    <input type="email" name="email">
    <input type="password" name="password">
    
    @guest('admin')
      <x-turnstile::widget />    
    @endguest
    
    <button type="submit">
        Login
    </button>
</form>

Middleware accepts failed challenges

You can allow the route to continue even if the challenge failed using the acceptFailed() method.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;

Route::post('comment', function (Request $request) {
    // ...
})->middleware(TurnstileMiddleware::acceptFailed());

Middleware checks action

If you have multiple Cloudflare Turnstile widgets in your application, and you have separated them through actions names, you can add a check to match the action name in the backend. If the action doesn't match, a validation exception will be thrown.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;

Route::post('comment', function (Request $request) {
    // ...
})->middleware(TurnstileMiddleware::action('comment:store'));

Validating on Precognitive

By default, the middleware will skip running on Precognitive requests. If you want to run it, set the TurnstileMiddleware::onPrecognitive() option, especially if your validation has side effects.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;

Route::post('comment', function (Request $request) {
    // ...
})->middleware(TurnstileMiddleware::onPrecognitive());

Validating with Rule

You can use the turnstile rule to check if the Turnstile challenge is present and is successful in the data to validate. The easiest way is to unpack the default rule contained in the rules() method of the Turnstile facade.

use Illuminate\Http\Request;
use Laragear\Turnstile\Facades\Turnstile;

public function create(Request $request)
{
    $request->validate([
        'comment' => 'required|string',
        // 'cf-turnstile-response' => 'turnstile',
        ...Turnstile::rules()
    ]);
        
    // ...
}

For more granular control, you can use the key method of the Turnstile facade to use the default keythat the Cloudflare Turnstile script injects into the form, and put your own additional validation rules if necessary.

use App\Rules\MyCustomRule;
use Illuminate\Http\Request;
use Laragear\Turnstile\Facades\Turnstile;

public function create(Request $request)
{
    $request->validate([
        // ...
        Turnstile::key() => ['turnstile', new MyCustomRule],
    ]);
        
    // ...
}

Rule bypass when authenticated

If you want to bypass the rule check if the user is authenticated, set the auth parameter on the rule.

use Illuminate\Http\Request;
use Laragear\Turnstile\Facades\Turnstile;

public function create(Request $request)
{
    $request->validate([
        Turnstile::key() => 'turnstile:auth',
    ]);
        
    // ...
}

You may also add a list of guards to check by adding them after = and separating them by ,.

$request->validate([
    Turnstile::key() => 'turnstile:auth=admin,developer',
]);

Rule accepts failed challenges

The rule supports not checking if the challenge is successful by setting the accept-failed parameter. This can be useful to retrieve the response later and programmatically continue based on the response result through the sucess() and failed() methods of the Turnstile facade.

use Illuminate\Http\Request;
use Laragear\Turnstile\Facades\Turnstile;

public function create(Request $request)
{
    $request->validate([
        Turnstile::key() => 'required|turnstile:accept-failed'
    ]);
        
    if (Turnstile::success()) {
        // ...
    }
}

Validating Manually

Important

The challenge is automatically retrieved by the request, middleware and rule. If that's case, you may use the challenge() method instead.

To validate the Challenge manually, you require the Turnstile Response Token that is sent by the frontend, and optionally the IP of the Request.

Once identified, you should use the getChallenge() method of Turnstile facade to retrieve the Challenge from Cloudflare Turnstile servers.

You will receive a Laragear\Turnstile\Challenge instance with some useful helpers to check the challenge status.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Facades\Turnstile;

Route::post('comment', function (Request $request) {
    $challenge = Turnstile::getChallenge(
        $request->input('cf-turnstile-response'), $request->ip()
    );
    
    if ($challenge->failed || $challenge->isNotAction('comment:store')) {
        // ... throw an exception.
    }
    
    // ... save the comment.
})

Alternatively, if you're already using the default configuration, you can just use getChallengeFromRequest() which will automatically resolve the Request from the Container and find the token using the default key name.

use Laragear\Turnstile\Facades\Turnstile;

$challenge = Turnstile::getChallengeFromRequest();

Once the challenge is retrieved, is saved into the Application Container. This makes easier to retrieve the challenge elsewhere in your application. If you don't want to save the Challenge, set the save parameter to false.

use Laragear\Turnstile\Facades\Turnstile;

$challenge = Turnstile::getChallenge('token', save: false);

Idempotency Keys

Because Cloudflare Turnstile Siteverify API will return an error when retrieving the same Challenge more than once, an idempotency key can be used in case of duplicate submissions.

How idempotency is handled will be up to your application. While most of the time is not needed at all, on some frontends the token may be resent anyway. To avoid errors, you can add a UUID string to both getChallenge() and getChallengeFromRequest() methods of the Turnstile facade.

use App\Uuid;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Challenge;
use Laragear\Turnstile\Facades\Turnstile;

Route::post('comment', function (Request $request) {
    // Generate a UUID using the hash of the input as the seed.
    $uuid = Uuid::generateWithSeed(md5(json_encode($request->input('body')));

    Turnstile::getChallengeFromRequest(idempotencyKey: $uuid);
        
    // ...
});

Getting the correct client IP

If you're under a Cloudflare Proxy, can get the correct client IP through the CF-Connecting-IP header. This is set as a constant in the Turnstile class, so you can use it when retrieving the challenge:

use App\Uuid;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Challenge;use Laragear\Turnstile\Turnstile;

Route::post('comment', function (Request $request, Turnstile $turnstile) {
    $challenge = $turnstile->getChallenge(
        $request->input($turnstile->key()),
        $request->header(Turnstile::HEADER)
    );
        
    // ...
});

Retrieving the Challenge on failure

If there is a server or backend error, the challenge retrieval will fail. If you still want to proceed, you may capture the exception and retrieve the Challenge with a try-catch block.

use Laragear\Turnstile\Exceptions\InvalidChallengeException;use Laragear\Turnstile\Facades\Turnstile;

try {
    $challenge = Turnstile::getChallenge();
} catch (InvalidChallengeException $exception) {
    $challenge = $exception->getChallenge();
}

Retrieving an already received Challenge

The challenge() method of the Turnstile facade can be used to retrieve an already saved Turnstile Challenge inside the Application Container.

use Laragear\Turnstile\Facades\Turnstile;

$challenge = Turnstile::challenge();

If you're not sure if the Challenge was received and saved, you can use both hasChallenge() and missingChallenge() beforehand.

use Laragear\Turnstile\Facades\Turnstile;

if (Turnstile::missingChallenge()) {
    return Turnstile::getChallengeFromRequest(); 
}

return Turnstile::challenge();

Alternatively, you can use both success() and failed() methods to check if the challenge is successful or has failed, respectively. Of course, these must be invoked after the challenge have been retrieved.

use App\Models\Comment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Facades\Turnstile;

Route::middleware('turnstile:accept-failed')
    ->post('comment', function (Request $request) {
        $request->validate([
            'body' => 'required|string',
        ])
        
        $comment = Comment::make($request->only('body'));
        
        // If the challenge is successful, show the comment.
        if (Turnstile::success()) {
            $comment->approved_at = now();
        }
        
        $comment->save();
        
        return back();
    });

Finally, you can always inject the Laragear\Tunrstile\Challenge anywhere in your application. For example, in your route controller action.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Challenge;

Route::middleware('turnstile')
    ->post('comment', function (Request $request, Challenge $challenge) {
        if ($challenge->isAction('comment:store')) {
            // ....
        }
        
        // ...
    });

Interstitial challenge

You can force a first-time visitor to complete a Turnstile challenge with the turnstile.interstitial middleware. Once completed, the user will be redirected to its intended route.

Before using it, you should register the default routes to handle to show interstitial challenge and capture it. You can do this with the Laragear\Turnstile\Http\Controllers\InterstitialController::register() method.

use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Controllers\InterstitialController;

Route::group(function () {
    // ...
})->middleware('turnstile.interstitial');

InterstitialController::register();

You may change the default path the routes will use using the first parameter, and middleware using the second:

use Laragear\Turnstile\Http\Controllers\InterstitialController;

InterstitialController::register('/challenge', 'guest');

Important

The interstitial middleware will throw a JSON response if the request requires JSON. This is because JSON response cannot be redirected. Instead, use the redirect_url key of the response to redirect the user to the interstitial controller.

response = await $fetch('/comment', 'POST', {body: 'My Comment'});

if (response.isStatus(400) && response.hasJson('redirect_url')) {
  window.location.href = response.json('redirect_url')
}

Skip when authenticated

If you want to skip the challenge if a user is authenticated, you may add the auth keyword as parameter.

use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Middleware\InterstitialMiddleware;

Route::get('photos', function () {
    // ..
})->middleware('turnstile.interstitial:auth');

Alternatively, you can set which guards to check to skip the middleware by setting the guards as auth=guard&guard...

use Illuminate\Support\Facades\Route;
use Laragear\Turnstile\Http\Middleware\InterstitialMiddleware;

Route::get('photos', function () {
    // ..
})->middleware('turnstile.interstitial:auth=web&admins');

Important

When setting the middleware to be skipped for authenticated users, interstitial routes should also be hidden for authenticated users.

use Laragear\Turnstile\Http\Controllers\InterstitialController;

InterstitialController::register(middleware: 'guest');

Global interstitial

If you want to register the middleware globally, you should do it in the web middleware group. This can be done in your bootstrap/app.php file or App\Providers\AppServiceProviders.

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();

Advanced configuration

Laragear Turnstile is intended to work out-of-the-box, but you can publish the configuration file for fine-tuning the Challenge verification.

php artisan vendor:publish --provider="Laragear\Turnstile\TurnstileServiceProvider" --tag="config"

You will get a config file with this array:

<?php

return [
    'env' => env('TURNSTILE_ENV', env('APP_ENV')),
    'key' => \Laragear\Turnstile\Turnstile::KEY,
    'client' => [
        \GuzzleHttp\RequestOptions::VERSION => 1.1,
    ],
    'site_key' => env('TURNSTILE_SITE_KEY'),
    'secret_key' => env('TURNSTILE_SECRET_KEY'),
    'interstitial' => [
        'key' => '_turnstile.interstitial',
        'view' => 'turnstile::interstitial',
        'route' => 'turnstile.interstitial',
        'duration' => true,
    ],
];

Environment

return [
    'env' => env('TURNSTILE_ENV'),
];

This sets which environment the library should run as. When null, it will mirror your current application environment.

  • On production, you will require your Site Key and Secret Key from Cloudflare Turnstile.
  • On testing, challenges will be faked regardless, no keys needed.
  • On the rest of environments, testing keys will be automatically injected.

If you set false as the environment value, both script and widget won't be rendered, and the request, middleware and rule won't retrieve challenges.

Warning

When using manual validation with the environment set as false, you will receive a successful fake Challenge.

Form Key

return [
    'key' => \Laragear\Turnstile\Turnstile::KEY,
];

This sets the default key to check for in the Request for the Turnstile response. By default, is cf-turnstile-response, but if you're using a custom frontend, you may change it here.

HTTP Client options

return [
    'client' => [
        \GuzzleHttp\RequestOptions::VERSION => 1.1,
    ],
];

This array sets the options for the outgoing request to Cloudflare Turnstile servers. This is handled by Guzzle, which in turn will pass it to the underlying transport. Depending on your system, it will probably be cURL.

By default, it instructs Guzzle to use HTTP/1.1, but you can upgrade if available in your platform.

Credentials

return [
    'site_key' => env('TURNSTILE_SITE_KEY'),
    'secret_key' => env('TURNSTILE_SECRET_KEY'),
];

Here is the full array of Turnstile Site Key (public) and Secret Key (private) to use. These can be obtained through your Cloudflare Dashboard. Do not change the array unless you know what you're doing. If you want to set your keys, use the environment variables instead:

TURNSTILE_SITE_KEY=...
TURNSTILE_SECRET_KEY=...

Interstitial

'interstitial' => [
    'key' => '_turnstile.interstitial',
    'view' => 'turnstile::interstitial',
    'route' => 'turnstile.interstitial',
    'duration' => true,
],

This controls the interstitial middleware behavior:

  • key: Where in the session is stored the challenge reminder.
  • view: View to use to show the user as the interstitial challenge.
  • route: The name of the route the middleware should point to.
  • duration: Default duration to reminder the challenge. The true value will remind the challenge forever. An integer will remind the challenge for that amount of minutes.

Testing

On testing, or when the environment is testing, the library will automatically fake successful Challenges without contacting Cloudflare Turnstile servers.

You may create your own fake challenges easily using the fake() method of the Tunrstile facade. It accepts any of the Turnstile Challenge attributes, which is great to test multiple responses from Turnstile in your application.

use Laragear\Turnstile\Facades\Turnstile;

public function test_comment_is_moderated_when_bot_detected()
{
    Turnstile::fake([
        'success' => false
    ]);
    
    $this->post(['comment' => 'test_comment']);
    
    $this->assertDatabaseHas('comments', [
        'body' => 'test_comment',
        'is_moderated' => true    
    ])
}

Testing keys

This library incorporates the official testing Turnstile Site Keys and Secret Keys as the Laragear\Turnstile\Enums\SiteKey and Laragear\Turnstile\Enums\SecretKey, respectively.

The easiest way to change the testing keys on development is to change the environment variables on your .env files, as Laravel will automatically restart to pick up the changes. You can use the enum names, as these will be matched automatically to the corresponding testing key.

TURNSTILE_SITE_KEY=ForceInteraction
TURNSTILE_SECRET_KEY=Fails

For the case of the widget, you may require to refresh your browser so the widget gets re-rendered with the selected key.

Note

This doesn't work on production environments. Ensure you have your correct keys in production!

Inside your application, you can programmatically swap keys use the useTestingSiteKey() and useTestingSecretKey() methods of the Turnstile facade, along with the corresponding enums for the behaviour you require to check.

use Laragear\Turnstile\Enums\SiteKey;
use Laragear\Turnstile\Enums\SecretKey;
use Laragear\Turnstile\Facades\Turnstile;

Turnstile::useTestingSiteKey(SiteKey::ForceInteraction);
Turnstile::useTestingSecretKey(SecretKey::Fails);

For the case of the widget, you can change the site key using the site-key attribute with the enum case name as value, in either kebab-case, snake_case, camelCase or StudlyCaps (these are normalized for you).

<x-turnstile::widget site-key="force-interaction" />

Laravel Octane compatibility

  • There are no singletons using a stale application instance.
  • There are no singletons using a stale config instance.
  • There are no singletons using a stale request instance.
  • There are no static properties written during a request.

There should be no problems using this package with Laravel Octane as intended.

HTTP/2 or HTTP/3 and cURL

To use HTTP/3, ensure you're using PHP 8.2 or later. cURL version 7.66 supports HTTP/3, and latest PHP 8.2 uses version 7.85.

For more information about checking if your platform can make HTTP/3 requests, check this PHP Watch article.

This library uses HTTP/1.1 by default to ensure backwards compatibility with PHP 8.2.

Security

If you discover any security related issues, please report it using the online form.

License

This specific package version is licensed under the terms of the MIT License, at time of publishing.

Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2025 Laravel LLC.

Cloudflare and Cloudflare Turnstile are trademarks of Cloudflare, Inc. Copyright © 2009-2025.