Securing your Symfony forms with Google reCAPTCHA v2

Quentin Ferrer
7 min readMay 25, 2020

Google reCAPTCHA is a free service that protects your website from spam and bots. Using Symfony Form with Google reCAPTCHA code will be effective and efficient way of validating the user against bots.

Integrating Google reCAPTCHA is recommended compared to the custom captcha. Indeed, the Product Manager of reCAPTCHA at Google explained:

CAPTCHAs have long relied on the inability of robots to solve distorted text. However, our research recently showed that today’s artificial intelligence technology can solve even the most difficult variant of distorted text at 99.8 per cent accuracy. Thus distorted text, on its own, is no longer a dependable test.

Registering your site on Google ReCAPTCHA

Before starting, you need to register your site and get an API key pair on the Admin Console. It is used to invoke reCAPTCHA service on your website.

Setting the label

Use a label that will make it easy for you to identify the site.

Setting the reCAPTCHA type

Choose the type of reCAPTCHA v2 for your site. A site only works with a single reCAPTCHA site type.

There are different types of reCAPTCHA:

  • Checkbox: it requires the user to click a checkbox indicating the user is not a robot.
  • Invisible: it does not require the user to click on a checkbox, instead it’s invoked directly via a JavaScript API call. I recommend it that is less intrusive than the checkbox type.

Setting the domains

Your registration is restricted to the domains you enter here, plus any subdomains. A valid domain requires a host and must not include any path, port, query, or fragment.

Note that the localhost domain works for testing.

Configuring your Symfony application

Adding Google API Keys

Add your reCAPTCHA key and secret inside the .env file:

# .env
GOOGLE_RECAPTCHA_SITE_KEY=6LfoM_YUAAAAACd3tyP82gpjQRrjJar-AoHaCyVQ
GOOGLE_RECAPTCHA_SECRET_KEY=6LfoM_YUAAAAAD_e49c6kKAG2_EFYpKPcCWo3bCq

The GOOGLE_RECAPTCHA_SITE_KEY is used to invoke reCAPTCHA service on your site and GOOGLE_RECAPTCHA_SECRET_KEY authorizes communication between your application backend and the reCAPTCHA server to verify the user’s response.

Don’t use this key/secret. It’s only as an example here and are not valid!

Rendering the reCAPTCHA widget

Adding a Twig global variable

Because the site key must be used on the client-side, add a global variable with the key. It will be available in all Twig templates.

# config/packages/twig.yaml
twig:
globals:
gg_recaptcha_site_key: '%env(GOOGLE_RECAPTCHA_SITE_KEY)%'

Creating a Form Type class

To reuse the reCAPTCHA in different forms, the best way is to create a form type ReCaptchaType.

// src/App/Form/ReCaptchaType.php
namespace App\Form;

use Symfony\Component\Form\AbstractType;

class ReCaptchaType extends AbstractType
{
}

If you’re using the default services.yaml configuration, nothing to do! Otherwise, create a service for this form class and tag it with form.type.

Making the ReCaptchaType configurable

Add reCAPTCHA type as an option. By default, it will be invisible:

// src/App/Form/ReCaptchaType.php
namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ReCaptchaType extends AbstractType
{
/**
*
@inheritDoc
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['type'] = $options['type'];
}
/**
*
@inheritDoc
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('type', 'invisible')
->setAllowedValues('type', ['checkbox', 'invisible']);
}
}

Creating the Form Type template

You need to custom the template to add the reCAPTCHA attributes to the HTML form.

First, create a new Twig template anywhere in the application to store the fragments used to render the types and updates the form_themes option to add this new template:

# config/packages/twig.yaml
twig:
form_themes:
- 'form/custom_types.html.twig'
- '...'

Create the form type template:

{# templates/form/custom_types.html.twig #}
{% block re_captcha_row %}
<div id="{{ id }}" data-toggle="recaptcha" data-type="{{ type }}">
</div>
{% endblock %}

Each element with data-toggle="recaptcha" will be rendered as a reCAPTCHA widget.

Adding reCAPTCHA JavaScript Library

To manage the different ReCAPTCHA’s types, the widget will be rendered explicitly by using the Google JavaScript API. It means that you need to add a callback function to manually render the widget. It will be executed once the Google dependencies have loaded.

Insert the Google JavaScript resource in your template with Google the onload, render and hl parameters. It contains all the logic for reCAPTCHA.

# templates/base.html.twig
<body>
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onGoogleReCaptchaApiLoad&render=explicit&hl={{app.request.locale}}" async defer></script>
</body>

Explicitly render the reCaptcha

Create the callback onGoogleReCaptchaApiLoad() function before the reCAPTCHA API loads. Each element with data-toggle="recaptcha" will be rendered as a widget.

<body>
<script type="text/javascript">
/**
* The callback function executed
* once all the Google dependencies have loaded
*/
function onGoogleReCaptchaApiLoad() {
var widgets = document.querySelectorAll('[data-toggle="recaptcha"]');
for (var i = 0; i < widgets.length; i++) {
renderReCaptcha(widgets[i]);
}
}
</script>
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onGoogleReCaptchaApiLoad&render=explicit&hl={{app.request.locale}}" async defer></script>
</body>

Create the renderReCaptcha(widget) function that will be used to render the widget thanks to the data-type attribute.

/**
* Render the given widget as a reCAPTCHA
* from the data-type attribute
*/
function renderReCaptcha(widget) {
var form = widget.closest('form');
var widgetType = widget.getAttribute('data-type');
var widgetParameters = {
'sitekey': '{{ gg_recaptcha_site_key }}'
};

if (widgetType == 'invisible') {
widgetParameters['callback'] = function () {
form.submit()
};
widgetParameters['size'] = "invisible";
}

var widgetId = grecaptcha.render(widget, widgetParameters);

if (widgetType == 'invisible') {
bindChallengeToSubmitButtons(form, widgetId);
}
}

For the invisible reCAPTCHA, when the user submits the form, this event must be canceled and you need to invoke the challenge and waiting for the Google response. Then the form can be submitted thanks to the callback function.

Create the bindChallengeToSubmitButtons(form, widgetId) function to invoke the challenge and prevent the submit buttons from submitting the form:

/**
* Prevent the submit buttons from submitting a form
* and invoke the challenge for the given captcha id
*/
function bindChallengeToSubmitButtons(form, reCaptchaId) {
getSubmitButtons(form).forEach(function (button) {
button.addEventListener('click', function (e) {
e.preventDefault();
grecaptcha.execute(reCaptchaId);
});
});
}

To finish, create the getSubmitButtons(form) function:

/**
* Get the submit buttons from the given form
*/
function getSubmitButtons(form) {
var buttons = form.querySelectorAll('button, input');
var submitButtons = [];

for (var i= 0; i < buttons.length; i++) {
var button = buttons[i];
if (button.getAttribute('type') == 'submit') {
submitButtons.push(button);
}
}

return submitButtons;
}

I recommend you to move the code logic into a captcha.js file and manage JavaScript with the Webpack Encore or Asset components.

# templates/base.html.twig
<body>
<script type="text/javascript" src="{{ asset('js/recaptcha.js') }}"></script>
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?onload=onGoogleReCaptchaApiLoad&render=explicit&hl={{ app.request.locale }}" async defer></script>
</body>

Using the ReCaptchaType to your Forms

Now, you are ready to use the form type to your forms. Try it !

$builder->add('captcha', ReCaptchaType::class, [
'type' => 'invisible' // (invisible, checkbox)
]);

Verifying the user’s response

When the form gets submit to Server, the Google script will send g-recaptcha-response as a POST data. You need to verify it in order to see whether the user is human or not.

Note that a reCAPTCHA user response token is valid for two minutes.

Installing reCAPTCHA PHP Library

Run the following command from your project directory:

composer require google/recaptcha "^1.2"

Registering reCAPTCHA class as Service

The library provides a reCAPTCHA class that needs the secret key.

# config/services.yamlservices:
ReCaptcha\ReCaptcha:
arguments:
- '%env(GOOGLE_RECAPTCHA_SECRET_KEY)%'

Creating a reCAPTCHA Validation Listener

The form events are able to control the form at different steps of the workflow. The form event POST_SUBMIT will be used to verify the user’s response. If the result is not a success, a FormError will be created on the reCaptchaType form type to make the form root invalid.

To make it, create an Event Subscriber to the FormEvents::POST_SUBMIT :

namespace App\Form\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ReCaptchaValidationListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
FormEvents::POST_SUBMIT => 'onPostSubmit'
];
}
public function onPostSubmit(FormEvent $event)
{
// verify the user's response
}
}

You need the ReCaptcha service to check the user’s response. The Request object is used to get the server name, the client IP and the user’s response g-recaptcha-response sent to the POST data.

<?php

namespace App\Form\EventListener;

use ReCaptcha\ReCaptcha;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\HttpFoundation\Request;

class ReCaptchaValidationListener implements EventSubscriberInterface
{
private $reCaptcha;

public function __construct(ReCaptcha $reCaptcha)
{
$this->reCaptcha = $reCaptcha;
}

public static function getSubscribedEvents()
{
return [
FormEvents::POST_SUBMIT => 'onPostSubmit'
];
}

public function onPostSubmit(FormEvent $event)
{
$request = Request::createFromGlobals();

$result = $this->reCaptcha
->setExpectedHostname($request->getHost())
->verify($request->request->get('g-recaptcha-response'), $request->getClientIp());

if (!$result->isSuccess()) {
$event->getForm()->addError(new FormError('The captcha is invalid. Please try again.'));
}
}
}

Registering reCAPTCHA Validation Listener to your Form Type

Now, add this event subscriber to your ReCaptchaType form type.

<?php

namespace App\Form;

use App\Form\EventListener\ReCaptchaValidationListener;
use ReCaptcha\ReCaptcha;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* Class ReCaptchaType.
*/
class ReCaptchaType extends AbstractType
{
/**
*
@var ReCaptcha
*/
private $reCaptcha;

/**
* ReCaptchaType constructor.
*
*
@param ReCaptcha $reCaptcha
*/
public function __construct(ReCaptcha $reCaptcha)
{
$this->reCaptcha = $reCaptcha;
}

/**
*
@inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventSubscriber(new ReCaptchaValidationListener($this->reCaptcha));
}
// ...
}

You are done!

What’s next?

Resources

Quentin Ferrer
Quentin Ferrer

Written by Quentin Ferrer

I’m a french web developer. I develop in PHP with the popular Symfony framework.

Responses (2)

Write a response