Strengthening Laravel Forms: A Step-by-Step Guide to Implementing reCAPTCHA for Bot Protection
Opinionated code Laravel code for adding recaptcha on website. Bot protection especially on open form (without user authentication)
- Create blade component for ease of reuse
resources/views/components/input/captcha.blade.php
@props([
'id',
'name',
'action'
])
{{--
<x-input.captcha
name="captcha"
id="captcha_user"
action="user_create"
/>
--}}
<script>
(function () {
const id = @json($id);
const captchaKey = @json(config('services.google.captcha.public_key'));
const action = @json($action);
const onLoaded = () => {
console.log('recaptcha on loaded')
if (!window.grecaptcha) {
console.error('Google catcha not loaded. Aborting...')
return;
}
grecaptcha.ready(function() {
console.log('grecaptcha ready')
grecaptcha.execute(captchaKey, { action }).then(function(token) {
const captchaInput = document.querySelector('#' + id) || {}
captchaInput.value = token
console.log({token})
});
});
};
const resource = 'https://www.google.com/recaptcha/api.js?render=' + captchaKey;
const container = document.head;
let script = container.querySelector(`script[data-captcha=google]`);
if (script) {
onLoaded()
return;
}
script = document.createElement('script')
script.src = resource
script.setAttribute('data-captcha', 'google')
script.addEventListener('load', onLoaded)
container.appendChild(script)
if (document.readyState === 'complete') {
onLoaded()
} else {
document.addEventListener('DOMContentLoaded', onLoaded)
}
})()
</script>
<input type="hidden" id="{{ $id }}" name="{{ $name }}">
- Add to form markup
<form action="{{ route('user.store') }}" class="form" method="post">
@csrf
...
<x-input.captcha
name="captcha"
id="captcha_user"
action="user_create"
/>
</form>
- Add form request
// ContactRequest.php
public function rules(): array
{
return [
// ... your other fields
'captcha_user' => [
'required',
]
];
}
public function messages(): array
{
return [
'captcha_user' => __('validation.captcha')
];
}
public function validateCaptcha(): bool
{
return app(CaptchaClient::class)->verify(
$this->input('captcha'),
$this->input('captcha_action')
);
}
- Add google recaptcha client
// CaptchaClient
<?php
namespace Neptune\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
class CaptchaClient
{
private string $secret;
/**
* CaptchaService constructor.
* @param $secret
*/
public function __construct($secret)
{
$this->secret = $secret;
}
public function verify($action, $input)
{
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => $this->secret,
'response' => $input,
])
->json();
$isSuccess = $response['success'];
if (! $response['success']) {
throw ValidationException::withMessages([
'captcha' => __('validation.captcha', ['reason' => $response['error-codes'][0]]),
]);
}
if ($response['action'] != $action) {
throw ValidationException::withMessages([
'captcha' => __('validation.captcha', ['reason' => 'action mismatch']),
]);
}
if ($response['score'] < .8) {
throw ValidationException::withMessages([
'captcha' => __('validation.captcha', ['reason' => 'score did not pass minimum mark']),
]);
}
}
}
- Add handler code in controller
// Controller
public function store (ContactRequest $request) {
$request->validateCaptcha();
// further logic
}
- Lang resource for validation error message
// lang/en/validation.php
<?php
return [
'captcha' => "The captcha has failed for some reasons: ':reason'.",
'custom' => [
'captcha' => [
'required' => 'Please help me know if you are human by filling the captcha challenge',
],
],
];
- Define config values
// config/services.php
<?php
return [
'google' => [
'captcha' => [
'public_key' => env('GOOGLE_CAPTCHA_PUBLIC_KEY'),
'secret_key' => env('GOOGLE_CAPTCHA_SECRET_KEY'),
],
],
];
All good now your form should resist bots a bit better
No more "No, I am not a robot"
If you did not have one, you will need to create an app here:
-
no more captcha. Verification happen under the hood
- before:
- after:
-
You can check google answer I consider action: "contactPage", score > .8 to be a valid request. You can use your own
You can give the user ip as optional param. (I don't. Google has already enough data on us)
The front end code The back end code Yes I am using @laravelphp.
I also added a new page /contacted to have a better reporting from google analytics conversion for specific goals. Don't worry I protected it, you can't access it except you are coming from the contact form Let me elaborate: Having a specific page where the user is being redirected is a good practice for analytics since you can then record the hits you are getting from that specific page (for me /contacted). You can have the same for user registration /registred
Wrap up
It was amazingly fast. The migration v2 -> v3 is straightforward. So if you did not, go ahead it is less than 5min. Next step for me is to add it on my login form as well. Happy coding!
Resources:
- display: https://developers.google.com/recaptcha/docs/v3
- verify: https://developers.google.com/recaptcha/docs/verify/
Update: April 8 2024
Recently I've noticed this very website is receiving spam message from the contact form. It appears like bots have grown in sophistication and now able to go past this implentation. One of the things I liked about it, is how seemless it is for the user while still managing to block bot. Might be a good idea to check the score and make sure it is still above 80%. Will update here