Skip to content
All posts
LaravelSecurity

Laravel Google ReCaptcha Enterprise with Score-based approach

July 21, 2022·Read on Medium·

Implement Google ReCaptcha Enterprise with score-based approach 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 score-based work

This ReCaptcha Enterprise works basically the same as ReCaptcha V3 which without user interaction and only based on score but more security enhancement. In short, this score-based reCaptcha 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.

Implement 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 without any user interaction, only based on score.

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 ignore all the options. 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, we need a hidden input to put value after we get the token from the callback function. Unlike Checkbox challenge, it already render together. You can simply add a section in your form with below code

<input type="hidden" name="g-recaptcha-response" id="hidden-token"/>
<input type="hidden" name="g-recaptcha-action" id="hidden-action"/>

and alter your submit button with this

<button data-sitekey="{{ config('services.recaptcha_ent.site_key') }}"
data-callback='onSubmit'
data-action='login'
class="g-recaptcha">
Log In
</button>

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

<script src="https://www.google.com/recaptcha/enterprise.js?render={{ config('services.recaptcha_ent.site_key') }}"></script>
<script>
function onSubmit(token) {
document.getElementById('hidden-token').value = token;
document.getElementById('hidden-action').value = "login";

document.getElementById("
login-form").submit();
}
</script>

Notice that, we also submit the action of the request. It is because the verification API parameter need those action.

Based on the script, it will automatically render once you click the submit button. If you want to customize, 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 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">
                    <input type="hidden" name="g-recaptcha-response" id="hidden-token"/>
                    <input type="hidden" name="g-recaptcha-action" id="hidden-action"/>
                </div>
                <div class="mt-10">
                    <button
                        data-sitekey="{{ config('services.recaptcha_ent.site_key') }}"
                        data-callback='onSubmit'
                        data-action='login'
                        class="g-recaptcha 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?render={{ config('services.recaptcha_ent.site_key') }}"></script>
        <script>
            function onSubmit(token) {
                document.getElementById('hidden-token').value = token;
                document.getElementById('hidden-action').value = "login";
                document.getElementById("login-form").submit();
            }
        </script>
    @endpush
</x-guest-layout>

If you have successfully place the script, it should display a floating badge like this

It is quite annoying to show the badge floating in the page. We can easily hide it with a CSS

.grecaptcha-badge { visibility: hidden; }

And the badge are gone. But there is condition where Google mention that in here

You are allowed to hide the badge as long as you include the reCAPTCHA branding visibly in the user flow

So basically you should always have such legal information legally checked before doing anything.

Now, 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",
"expectedAction": "Value from g-recaptcha-action"
}
}

And the response should be like below

[
"name" => "projects/1037628514823/assessments/ab913a9764000000"
"event" => array:6 [
"token" => "TOKEN"
"siteKey" => "SITE KEY"
"userAgent" => ""
"userIpAddress" => ""
"expectedAction" => "login"
"hashedAccountId" => ""
]
"riskAnalysis" => [
"score" => 0.9
"reasons" => []
]
"tokenProperties" => [
"valid" => true
"invalidReason" => "INVALID_REASON_UNSPECIFIED"
"hostname" => "localhost"
"action" => "login"
"createTime" => "2022-07-14T07:07:15.050Z"
]

]

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(),
                'expectedAction' => request()->get('g-recaptcha-action', null),
            ]
        ]);

        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');
    }
}

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-action' => 'required|string',
'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 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.

Just like that. Simple~ Hope it helps

Notes

To improve reCAPTCHA Enterprise’s risk model, Google recommend that you include reCAPTCHA Enterprise on every page of your site because it helps in understanding how real users and bots transition between different pages and actions.

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