Strengthening Laravel Forms: A Step-by-Step Guide to Implementing reCAPTCHA for Bot Protection

Aug 04, 2021 / 4 min read

Opinionated code Laravel code for adding recaptcha on website. Bot protection especially on open form (without user authentication)

  1. 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 }}">
  1. 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>
  1. 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')
        );
    }
  1. 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']),
            ]);
        }
    }
}

  1. Add handler code in controller
// Controller

public function store (ContactRequest $request) {
        $request->validateCaptcha();
        // further logic
}
  1. 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',
			],
	],
];
  1. 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: Google create

  • no more captcha. Verification happen under the hood

    • before: form before
    • after: form after
  • You can check google answer google response 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 front The back end code back 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 twitter 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:

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