Laravel Backpack is one of the most powerful admin panels for building your custom project. Laravel 11, combined with Laravel Backpack...
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.
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.
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…
Follow this link: https://developers.cloudflare.com/turnstile/get-started/ to obtain the Cloudflare Turnstile sitekey and secret key.
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"
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,
];
}
}
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.',
};
}
}
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.
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
);
}
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!
Subscribe to our "Article Digest". We'll send you a list of the new articles, every week, month or quarter - your choice.
What do you think about this?
Wondering what our community has been up to?