Skip to content
All posts
LaravelSecurity

Laravel Google ReCaptcha Enterprise with Checkbox

July 20, 2022·Read on Medium·

Implement Google ReCaptcha Enterprise v2 with Checkbox in Laravel

What is Google’s ReCaptcha Enterprise?

ReCaptcha Enterprise is built on the existing reCAPTCHA API and it uses advanced risk analysis techniques to distinguish between humans and bots. It include website protection from spam and abuse and detect other types of fraudulent activities on the sites such as credential stuffing, account takeover (ATO) and automated account creation.

ReCaptcha Enterprise offers more features and new enhancement in term of detection with more granular scores, reason codes for risky events, mobile app SDKs, password breach/leak detection, Multi-factor authentication (MFA), and the ability to tune your site-specific model to protect enterprise businesses.

For features comparison between normal reCaptcha and reCaptcha Enterprise, you may visit this link

How checkbox-based work

ReCaptcha Enterprise (Checkbox-based) works exactly the same as ReCaptcha V2 but more security enhancement. Basically, Checkbox challenge use to verify user interactions on website pages and mobile applications. Checkbox key type return a score for each request which is based on interactions with your site or application. This score lets you understand the level of risk that the interaction poses and helps you to take appropriate actions for your site or application. You may refer this diagram for further understanding.

Implementation in Laravel

Well, in this experiment we will try to add Google ReCaptcha Enterprise to a login form without using any package. I will show how to implement reCaptcha Enterprise with checkbox challenge.

Here are the steps:

  1. Enable ReCaptcha API
  2. Create new reCaptcha Enterprise Key
  3. Create new Credential API Key
  4. Add keys to config file
  5. Implement reCaptcha scripting at Login page
  6. Create a Laravel Rule
  7. Add Validation rule to Login logic
  8. Testing and validation

Lets get started

Enable ReCaptcha API

For this step, i assume you already have google account. Go to this page and if you don’t have any project created yet, there will be a popup like below.

Above page showing if you are the first time visit the google cloud console. You may follow the process.

At the top of the page, you will see the search box. Search “Recaptcha Enterprise”.

You will be redirect to this page. If you don’t have any project created yet, google will ask you to create it first. Click on the “Create Project” to proceed.

Fill in the form. Just ignore the warning sign and click “Create”.

You will be redirect to https://console.cloud.google.com/apis/library/recaptchaenterprise.googleapis.com. In order to use reCaptcha Enterprise, you need to enable the API. Just click the Enable button and wait until it finish.

Please read the Pricing if you consider using as business purpose.

You will redirect to https://console.cloud.google.com/security/recaptcha after complete enable. If not, just search again at the search bar. You will see you need to enable billing. Don’t worry because google will only charge you if the API usage exceed 1Million assessment per month.

Create new reCaptcha Enterprise Key

Now we can proceed to create the reCaptcha Key.

Click on the “+Create Key” at the top of the enterprise keys table

You can put any label you desired as long as you remember. For the domains, if you are developing in the localhost, put “localhost” or “127.0.0.1”. You will see 4 options there. We can enable only “Use checkbox challenge” since we want to use the “Im not robot” feature. The rest you may want to ignore or want to enable, its up to your cases. For testing purpose, you may disable domain verification to ease your testing. Click “Create Key” to proceed.

You will be showing page of how you can integrate in your website. We can ignore first and proceed to next step

Create new Credential API Key

For the ReCaptcha Enterprise, you are required to use API Key instead of Secret key (deprecated).

Go to APIs & Services and click Credentials.

Click create credentials and choose API key.

Google will create a key directly and you must configure it after that. Click on Edit API Key.

You may rename the label if you want. Optionally, you can restrict the API usage only for reCaptcha Enterprise purpose (Recommended). The others restrictions is depends on your requirement. Click Save and we good to proceed to the next step.

Add keys to config file

Now, in order to use reCaptcha Enterprise, we need

  1. Project ID
  2. API Key
  3. Site Key

All of them we already created it in the previous steps except the project ID.

The project ID can be retrieve when you click your project name at the top of the console page. Now, go ahead to your .env file and fill the values to the correct keys.

GOOGLE_RECAPTCHA_PROJECT_ID=XXXX
GOOGLE_RECAPTCHA_API_KEY=XXXX
GOOGLE_RECAPTCHA_SITE_KEY
=XXXX

After that, find a config file name services.php and add new lines with below code

<?php
return [
...
'recaptcha_ent' => [
'project_id' => env('GOOGLE_RECAPTCHA_PROJECT_ID'),
'api_key' => env('GOOGLE_RECAPTCHA_API_KEY'),
'site_key' => env('GOOGLE_RECAPTCHA_SITE_KEY'),
],
]

Implement reCaptcha scripting at Login page

Now, you can simply add a section in your form with below code

<div class="block mt-4">
<div class="g-recaptcha" data-sitekey="{{ config('services.recaptcha_ent.site_key') }}"></div>
</div>

and put below javascript at the bottom of the page or any desire place

<script src="https://www.google.com/recaptcha/enterprise.js" async defer></script>

Based on the script, it will automatically render the checkbox. If you required to customize explicitly, you may refer here.

Here is the sample login page i made based on Laravel Jetstream and Fortify

login.blade.php View on GitHub
<x-guest-layout>
    <x-jet-authentication-card>
        <x-slot name="logo">
            <x-jet-authentication-card-logo />
        </x-slot>

        <div class="p-3">
            <x-jet-validation-errors class="mb-4" />

            @if (session('status'))
                <div class="mb-4 font-medium text-sm text-green-600">
                    {{ session('status') }}
                </div>
            @endif
            @if (session('message'))
                <div class="mb-4 font-medium text-sm text-green-600">
                    {{ session('message') }}
                </div>
            @endif
            <form id="login-form" method="POST" action="{{ route('login') }}" x-data="{
                        usePhoneNumber : false
                    }">
                @csrf
                <x-honeypot />
                <div x-show="!usePhoneNumber">
                    <x-jet-label for="email" class="text-sm font-bold text-gray-700 tracking-wide" value="{{ __('Email') }}" />
                    <input id="email" class="w-full border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"  name="email" type="email" placeholder="biru@gmail.com" :required="!usePhoneNumber" :disabled="usePhoneNumber">
                    <a class="text-xs font-display font-semibold text--black hover:text--black cursor-pointer" @click="usePhoneNumber = true">
                        Use phone number instead
                    </a>
                </div>
                <div x-show="usePhoneNumber" x-cloak>
                    <x-jet-label for="phone_number" class="text-sm font-bold text-gray-700 tracking-wide" value="{{ __('Phone Number') }}" />
                    <input id="phone_number" class="w-full border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"  name="email" type="tel" placeholder="+60123485839" :required="usePhoneNumber" :disabled="!usePhoneNumber">
                    <a class="text-xs font-display font-semibold text--black hover:text--black cursor-pointer" @click="usePhoneNumber = false">
                        Use email instead
                    </a>
                </div>
                <div class="mt-8">
                    <div class="flex justify-between items-center">
                        <x-jet-label for="password" class="text-sm font-bold text-gray-700 tracking-wide" value="{{ __('Password') }}" />
                        @if (Route::has('password.request'))
                            <div>
                                <a class="text-xs font-display font-semibold text--black hover:text--black cursor-pointer"
                                   href="{{ route('password.request') }}">
                                    Forgot Password?
                                </a>
                            </div>
                        @endif
                    </div>
                    <input class="w-full border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm" name="password" type="password" placeholder="Enter your password" required>
                </div>
                <div class="block mt-4">
                    <label for="remember_me" class="flex items-center">
                        <x-jet-checkbox id="remember_me" name="remember" />
                        <span class="ml-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
                    </label>
                </div>
                <div class="block mt-4">
                    <div class="g-recaptcha" data-sitekey="{{ config('services.recaptcha_ent.site_key') }}"></div>
                </div>
                <div class="mt-10">
                    <button class="bg-black text-gray-100 p-3 w-full rounded-full tracking-wide font-semibold font-display focus:outline-none focus:shadow-outline hover:bg--black shadow-lg">
                        Log In
                    </button>
                </div>
                <div class="text-center text-xs mt-4 text-gray-600 mb-5">
                    Also you can login with
                </div>
                <div class="block flex items-center space-x-4 space-y-3 space-y-0">
                    <a href="{{ URL::signedRoute('connect-to-social-account', ['facebook']) }}" class="bg-[#4267B2] text-center text-white p-3 w-full rounded-full tracking-wide font-semibold focus:outline-none focus:shadow-outline drop-shadow-lg">
                        Facebook
                    </a>
                    <a href="{{ URL::signedRoute('connect-to-social-account', ['google']) }}"  type="button" class="bg-[#DB4437] text-center text-white p-3 w-full rounded-full tracking-wide font-semibold focus:outline-none focus:shadow-outline drop-shadow-lg">
                        Google
                    </a>
                </div>
            </form>
        </div>
    </x-jet-authentication-card>
    @push('scripts')
        <script src="https://www.google.com/recaptcha/enterprise.js" async defer></script>
    @endpush

</x-guest-layout>

If you have successfully place the script, it should display the checkbox like this

Lets say we dump the request, we should see g-recaptcha-response appended along with form input.

Now, we can proceed to validate the token which is valid or not.

Create a Laravel Rule

Based on documentation, each ReCaptcha response token is valid for 2 minutes and can only be verified once to prevent replay attacks. If you need a new token, you need to refresh or re-run the ReCaptcha verification.

After you get the response token, you need to verify it within 2 minutes with ReCaptcha using the following API to ensure the token is valid.

https://recaptchaenterprise.googleapis.com/v1/projects/PROJECT_ID/assessments?key=API_KEY (POST)

This API required JSON body with following content

{
"event": {
"token": "Token from g-recaptcha-response",
"siteKey": "GOOGLE_RECAPTCHA_SITE_KEY",
}
}

And the response should be like below

{
"tokenProperties": {
"valid": true,
"invalidReason": "INVALID_REASON_UNSPECIFIED",
"hostname": "localhost",
"action": "login",
"createTime": "2022-07-14T15:32:28.978Z"
},

"riskAnalysis": {
"score": 0.9,
"reasons": []
},
"event": {
"token": "TOKEN",
"siteKey": "KEY",
"expectedAction": "USER_ACTION",
"userAgent": "",
"userIpAddress": "",
"expectedAction": "login",
"hashedAccountId": ""
},
"name": "projects/PROJECT_ID/assessments/b1e3132764000000",
}

Now we know validation step and response, lets create a validation rule name ReCaptchaEnterpriseRule

php artisan make:rule ReCaptchaEnterpriseRule

And paste the below code.

ReCaptchaEnterpriseRule.php View on GitHub
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class ReCaptchaEnterpriseRule implements Rule
{
  
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $response = \Http::asJson()->post($this->recaptchaEnterpriseVerificationUrl(), [
            'event' => [
                'token' => $value,
                'site_key' => $this->getSiteKey(),
            ]
        ]);

        return $response->json('tokenProperties.valid');
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'Unable to validate recaptcha token';
    }

    private function recaptchaEnterpriseVerificationUrl(): string
    {
        return "https://recaptchaenterprise.googleapis.com/v1/projects/{$this->getProjectId()}/assessments?key={$this->getApiKey()}";
    }

    private function getProjectId(): ?string
    {
       return config('services.recaptcha_ent.project_id');
    }

    private function getApiKey(): ?string
    {
        return config('services.recaptcha_ent.api_key');
    }
    
    public function getSiteKey(): ?string
    {
        return config('services.recaptcha_ent.site_key');
    }

}

Note that, this rules only cater true or false condition based on the response. If you want to have extra validation, you may customize it

Add Validation rule to Login logic

Add extra validation in your controller logic to use the ReCaptchaEnterpriseRule

public function login(Request $request)
{
....
$request->validate([
'g-recaptcha-response' => ['required', new ReCaptchaEnterpriseRule]
]);
...
}

For more convenience, i recommended to put any validation in the Request class, so that your controller won’t get messy

Testing and validation

By the time you submit the form, you will be able to utilize the ReCaptcha Enterprise functionality successfully. If it is not working, i suggest you to debug or dump the request from the verification API to see the error messages or error codes provided in the response.

Here are the sample of when we are unable to validate the recaptcha token.

Just like that. Simple~ Hope it helps

Notes

If you want to use checkbox site keys with CAPTCHA challenges to protect against automated attacks, be aware of the following caveats:

  • CAPTCHAs require user interaction, which increases friction and might decrease conversion rates.
  • Due to the advances in computer vision and machine intelligence, CAPTCHAs are becoming less useful to distinguish between humans and bots.
  • CAPTCHAs are also under threat from paid attackers who can solve all types of challenges.
  • CAPTCHAs are not accessible for all users, so they might not be suitable if your website has accessibility requirements.

It is recommended by Google to use Score-based reCaptcha implementation because of no user interaction and enhanced security features.

References

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium