DEV Community

Cover image for How to add Recaptcha to your Laravel App without a package
David
David

Posted on

How to add Recaptcha to your Laravel App without a package

Adding reCAPTCHA to your Laravel application is important to protect your forms from spam and bot attacks. In this article, I'll show you how to implement Google reCAPTCHA v2 in your Laravel authentication system.

Prerequisites

  • A Laravel application
  • A Google reCAPTCHA v2 site key and secret key. Here is how to get it quick:

Go to https://developers.google.com/recaptcha and click 'Get Started'

Image description

Enter your app information, choose the v2 -> 'Not a robot' option and click 'Submit'.

Image description

You will see the site and secret keys. Or you can access it in your console at https://console.cloud.google.com/security/recaptcha

Image description

If you want to know the difference between the v3 and the v2 options, ask Google ;)

Step 1: Configure reCAPTCHA Keys

First, add your reCAPTCHA configuration to your .env file:

RECAPTCHA_SITE_KEY=your_site_key_here
RECAPTCHA_SECRET_KEY=your_secret_key_here
Enter fullscreen mode Exit fullscreen mode

Create a new config file config/captcha.php:

<?php

return [
    'sitekey' => env('RECAPTCHA_SITE_KEY'),
    'secret' => env('RECAPTCHA_SECRET_KEY'),
];
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a Validation Trait

Create a new trait to handle reCAPTCHA validation:

<?php

namespace App\Traits;

use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;

trait RecaptchaValidation
{
    /**
     * Validate reCAPTCHA token for a specific action
     *
     * @param string $token
     * @param string $action
     * @return bool
     * @throws ValidationException
     */
    protected function validateRecaptcha(string $token, string $action = 'DEFAULT'): bool
    {
        // Retrieve environment variables for Google Cloud Project ID and reCAPTCHA Site Key
        $recaptchaKey = config('captcha.sitekey');
        $project = config('services.google.project_id');

        // Create the reCAPTCHA client and project name
        $options = [
            'credentials' => config('services.google.service_account_credentials'),
        ];

        try {
            $client = new RecaptchaEnterpriseServiceClient($options);
            $projectName = $client->projectName($project);

            // Set the event for the reCAPTCHA assessment
            $event = (new Event())
                ->setSiteKey($recaptchaKey)
                ->setToken($token);

            // Create the assessment
            $assessment = (new \Google\Cloud\RecaptchaEnterprise\V1\Assessment())
                ->setEvent($event);

            $response = $client->createAssessment($projectName, $assessment);

            // Check if the token is valid
            if (!$response->getTokenProperties()->getValid()) {
                $invalidReason = InvalidReason::name($response->getTokenProperties()->getInvalidReason());
                Log::error("reCAPTCHA token invalid: $invalidReason");
                return false;
            }

            // Check if the action matches the expected action
            if ($response->getTokenProperties()->getAction() !== $action) {
                Log::error("reCAPTCHA action mismatch. Action: $action and expected action: " . $response->getTokenProperties()->getAction());
                return false;
            }

            // Check the risk score
            $riskScore = $response->getRiskAnalysis()->getScore();
            Log::info("reCAPTCHA risk score for {$action}: {$riskScore}");

            return $riskScore >= config('app.recaptcha.threshold');

        } catch (\Exception $e) {
            Log::error("reCAPTCHA assessment failed for {$action}: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Validate reCAPTCHA token and throw exception if invalid
     *
     * @param string $token
     * @param string $action
     * @throws ValidationException
     */
    protected function validateRecaptchaOrFail(string $token, string $action = 'DEFAULT'): void
    {
        if (!$this->validateRecaptcha($token, $action)) {
            throw ValidationException::withMessages([
                'g-recaptcha-response' => ['reCAPTCHA verification failed.'],
            ]);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Add reCAPTCHA to Your Views

Add the reCAPTCHA script to your layout file (e.g., layouts/auth.blade.php):

<!DOCTYPE html>
<html>
<head>
    <!-- Other head elements -->
    <script src="https://www.google.com/recaptcha/enterprise.js" async defer></script>
</head>
<body>
    @yield('content')
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Add the reCAPTCHA widget to your login form (login.blade.php):

<form method="POST" action="{{ route('login') }}">
    @csrf
    <!-- Email and password fields -->

    <div class="form-group mt-4">
        @error('g-recaptcha-response')
            <span class="text-danger">{{ $message }}</span>
        @enderror
        <div class="g-recaptcha" data-sitekey="{{ config('captcha.sitekey') }}" data-action="LOGIN"></div>
    </div>

    <button type="submit" class="btn btn-primary">
        {{ trans('menu.login') }}
    </button>
</form>
Enter fullscreen mode Exit fullscreen mode

And to your registration form (register.blade.php):

<form method="POST" action="{{ route('register') }}">
    @csrf
    <!-- Name, email, and password fields -->

    <div class="form-group mt-4">
        @error('g-recaptcha-response')
            <span class="text-danger">{{ $message }}</span>
        @enderror
        <div class="g-recaptcha" data-sitekey="{{ config('captcha.sitekey') }}" data-action="REGISTER"></div>
    </div>

    <button type="submit" class="btn btn-primary">
        {{ trans('menu.sign_up') }}
    </button>
</form>
Enter fullscreen mode Exit fullscreen mode

Step 4: Update Controllers

Modify your LoginController.php:

use App\Traits\RecaptchaValidation;

class LoginController extends Controller
{
    use RecaptchaValidation;

    public function login(Request $request)
    {
        // Validate login credentials and reCAPTCHA token
        $this->validateLogin($request);

        $request->validate([
            'g-recaptcha-response' => 'required|string',
        ]);

        // Validate reCAPTCHA
        $this->validateRecaptchaOrFail(
            $request->input('g-recaptcha-response'),
            'LOGIN'
        );

        // Continue with login logic
        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

        return $this->sendFailedLoginResponse($request);
    }
}
Enter fullscreen mode Exit fullscreen mode

And your RegisterController.php:

use App\Traits\RecaptchaValidation;

class RegisterController extends Controller
{
    use RecaptchaValidation;

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
            'g-recaptcha-response' => ['required', 'string'],
        ]);
    }

    public function register(Request $request)
    {
        $this->validator($request->all())->validate();

        // Validate reCAPTCHA before registration
        $this->validateRecaptchaOrFail(
            $request->input('g-recaptcha-response'),
            'REGISTER'
        );

        event(new Registered($user = $this->create($request->all())));

        $this->guard()->login($user);

        return $this->registered($request, $user)
            ?: redirect($this->redirectPath());
    }
}
Enter fullscreen mode Exit fullscreen mode

Et voila! You're welcome.

Any thoughts or feedback on this?
Let me know in the comments.

Top comments (0)