How To Integrate Cloudflare Turnstile in Laravel Backpack without any package

Laravel Backpack is one of the most powerful admin panels for building your custom project. Laravel 11, combined with Laravel Backpack...

Kidd Tang
Kidd Tang
Share:

1_eVHnMum4ZlvG1ZNiA34k4g[1]

Laravel Backpack is one of the most powerful admin panels for building your custom project. Laravel 11, combined with Laravel Backpack v6 and the brand new Tabler theme, unlocks new potential in the classic stack of Bootstrap + jQuery for admin panels.

You may find this useful for your existing Backpack projects.


1_Z6mTLWnrtmJRdsEY9A2xVQ[1]

The outcome of this article is to add a Cloudflare Turnstile (a FREE Google reCAPTCHA alternative) to your Backpack Login page, without any third-party composer packages. It's arguable whether to use external libraries or not. My rule of thumb is: if there are no official packages and it's simple enough, try not to use third-party packages. But, this article isn't meant to discuss that dilemma.

Here is the official Turnstile documentation for a login example in three steps: Cloudflare Turnstile Login Page.

Now we need to add this to our Laravel Backpack login page.

Before You Begin: Install Laravel Backpack

Make sure you have installed Laravel 11 and Backpack v6.

composer require backpack/crud
php artisan backpack:install

After answering a series of questions, you'll have Laravel Backpack up and running…

Step 1: Configure sitekey and secret key

Follow this link: https://developers.cloudflare.com/turnstile/get-started/ to obtain the Cloudflare Turnstile sitekey and secret key.

1_jNkpgWFk1wo_AfpnJgBPHQ[1]

Add the config in your `config/services.php

'cloudflare_turnstile' => [
    'site_key' => env('TURNSTILE_SITE_KEY'),
    'secret'   => env('TURNSTILE_SECRET_KEY'),
],

After that, paste your keys in your .env file

TURNSTILE_SITE_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TURNSTILE_SECRET_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Step 2: Create Your Cloudflare Turnstile Client

We will create server-side functions that interact with the Cloudflare Turnstile service. These functions will send requests to the service and parse the responses we receive.

File #1: app/Services/CloudflareTurnstile/CloudflareTurnstileClient.php

You may place the client into your preferred namespace; it's just my convention.

This is the HTTP client to perform the backend verification after your form submission.

<?php

namespace App\Services\CloudflareTurnstile;

use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;

class CloudflareTurnstileClient
{
    private const TURNSTILE_VERIFY_ENDPOINT = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
    private const RETRY_ATTEMPTS = 3;
    private const RETRY_DELAY = 100;

    public function siteVerify(string $response): CloudflareTurnstileResponse
    {
        $verificationResponse = $this->sendTurnstileVerificationRequest($response);

        return $this->parseVerificationResponse($verificationResponse);
    }

    private function sendTurnstileVerificationRequest(string $response): Response
    {
        return Http::retry(self::RETRY_ATTEMPTS, self::RETRY_DELAY)
                   ->asForm()
                   ->acceptJson()
                   ->post(self::TURNSTILE_VERIFY_ENDPOINT, [
                       'secret'   => config('services.cloudflare_turnstile.secret'),
                       'response' => $response,
                   ]);
    }

    private function parseVerificationResponse(Response $response): CloudflareTurnstileResponse
    {
        if (!$response->ok()) {
            return new CloudflareTurnstileResponse(success: false, errorCodes: []);
        }

        return new CloudflareTurnstileResponse(
            success: $response->json('success'),
            errorCodes: $response->json('error-codes')
        );
    }
}

File #2: app/Services/CloudflareTurnstile/CloudflareTurnstileResponse.php

This is just a wrapper class for the response.

<?php

namespace App\Services\CloudflareTurnstile;

use JsonSerializable;

final readonly class CloudflareTurnstileResponse implements JsonSerializable
{
    public function __construct(
        private bool  $success,
        private array $errorCodes,
    )
    {
    }

    public function isSuccess(): bool
    {
        return $this->success;
    }

    public function getErrorCodes(): array
    {
        return $this->errorCodes;
    }

    public function jsonSerialize(): array
    {
        return [
            'success'     => $this->success,
            'error-codes' => $this->errorCodes,
        ];
    }
}

Step 3: Create a custom validation rule for Cloudflare Turnstile

Run the artisan command to generate the validation rule

php artisan make:rule CloudflareTurnstile

This is the CloudflareTurnstile.php in the Rules folder

<?php

namespace App\Rules;

use App\Services\CloudflareTurnstile\CloudflareTurnstileClient;
use App\Services\CloudflareTurnstile\CloudflareTurnstileResponse;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class CloudflareTurnstile implements ValidationRule
{
    private CloudflareTurnstileClient $turnstileClient;

    public function __construct()
    {
        $this->turnstileClient = app(CloudflareTurnstileClient::class);
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $response = $this->turnstileClient->siteVerify($value);

        if (!$response->isSuccess()) {
            $this->handleErrorCodes($response, $fail);
        }
    }

    private function handleErrorCodes(CloudflareTurnstileResponse $response, Closure $fail): void
    {
        foreach ($response->getErrorCodes() as $errorCode) {
            $fail($this->mapErrorCodeToMessage($errorCode));
        }
    }

    private 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.',
        };
    }
}

Step 4: Override the Login Blade file

Assume that you are using the default Tabler theme, we're going to modify the default login form from Backpack.

Copy the original form.blade.php login page from the vendor folder: vendor/backpack/theme-tabler/resources/views/auth/login/inc/form.blade.php (Or copy from Github: Laravel-Backpack/theme-tabler) to your resources folder: resources/views/vendor/backpack/theme-tabler/auth/login/inc/form.blade.php

For more information on overriding default components, you may refer to the official docs here.

Before the form-footer class, paste the Cloudflare Turnstile container

<div class="my-2 d-flex flex-column justify-content-center align-items-center">
    <div id="cf-turnstile-container">
        <div class="cf-turnstile" data-sitekey="{{ config('services.cloudflare_turnstile.site_key') }}"></div>
        <input type="hidden" id="turnstile-response" name="cf-turnstile-response" required>
    </div>
    @if ($errors->has('cf-turnstile-response'))
        <div class="text-danger">{{ $errors->first('cf-turnstile-response') }}</div>
    @endif
</div>

Also, inside the @section('after_scripts'), paste the necessary script for Cloudflare Turnstile rendering.

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

<script>
    turnstile.ready(function () {
        turnstile.render('#cf-turnstile-container', {
            'sitekey': '{{ config('services.cloudflare_turnstile.site_key') }}',
            'theme': 'light',
            'callback': function(token) {
                document.getElementById('turnstile-response').value = token;
            }
        });
    });
</script>

You may want to customize your rendering output. You can read the official docs for the available options: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations.

Step 5: Override the Login Page Controller

Create your new LoginController in app/Http/Controllers/Admin/Auth/LoginController.php

php artisan make:controller Admin/Auth/LoginController
<?php

namespace App\Http\Controllers\Admin\Auth;

use App\Rules\CloudflareTurnstile;
use Illuminate\Http\Request;

class LoginController extends \Backpack\CRUD\app\Http\Controllers\Auth\LoginController
{
    /**
     * Validate the user login request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function validateLogin(Request $request)
    {
        $request->validate([
            $this->username() => 'required|string',
            'password' => 'required|string',
            'cf-turnstile-response' => ['required', 'string', new CloudflareTurnstile()],
        ]);
    }
}

Extend the original LoginController and override only the validateLogin() method.

In your AppServiceProvider, tell Laravel to use your new controller in the boot() method.

public function boot(): void
{
    // some other code ...

    // Customize Controllers
    $this->app->bind(
        \Backpack\CRUD\app\Http\Controllers\Auth\LoginController::class,
        \App\Http\Controllers\Admin\Auth\LoginController::class
    );
}

Finally! Completed Login Form

image

Thanks for reading! I'm Kidd Tang, and I've been using Backpack for over 7 years now. Please feel free to leave your feedback in the comments below, and let me know what topics you'd like me to cover in the future. Your input helps me create content that's most useful to you! If you enjoyed this article, you might also like my other writings on Medium and my tutorial videos on YouTube. I'd love for you to check them out!

Want to receive more articles like this?

Subscribe to our "Article Digest". We'll send you a list of the new articles, every week, month or quarter - your choice.

Reactions & Comments

What do you think about this?

Thank you for the article. I notice some problems when using DevTools. "The GET method is not supported for route livewire/update. Supported methods: POST." It may be "CSRF token mismatch" but not sure. Just reporting it :)

Latest Articles

Wondering what our community has been up to?