'I am not a robot': reCaptcha v3 on a Symfony form

Written by NewTrick

06 Jul 2025

#symfony

A toy robot with other small toys unfocussed on the background

One of the most common ways to prevent spam, bots, and password stuffing attacks on your website is through the use of reCaptcha. You've undoubtedly seen it if you've ever been asked to click on tiles containing traffic lights or clicking the box 'I am not a robot'.

Unfortunately there are implications for the use of reCaptcha for accessibility for people who can't see the images or lack the motor control to click on the small boxes.

They also have a range of issues. Users give up on CAPTCHA 15% of the time, while bots were able to solve the puzzles 85% of the time. For people who used the audio versions of the puzzles, studies found that 3 users could only agree on the correct solution around 30% of the time.

They are also a minor irritant for all users, stopping the flow of whatever they are trying to do.

Thankfully there are alternate methods, which do not require user action. You may have encountered Cloudflare's Turnstile checking--which flashes on the page, checks to see whether user behaviour seems suspicious, and then allows you to move on.

There are undoubtedly issues with the automatic versions, but taking the user out of the equation seems like the most sensible approach.

Google reCaptcha v3

This post will look at how to add  Google's reCaptcha version 3 to a Symfony project, although the steps related to the front end are fairly generic so could be applied to other frameworks.

Instead of asking the user to complete a task like clicking on tiles that contain motorcycles or entering a difficult-to-read string of numbers and letters, reCaptcha v3 will verify the users on your site by ascribing a score to an action to determine whether it is likely to be a robot or a human. Thus it is a frictionless and invisible solution.

Prerequisites

Before starting, you will need a Google account if you don't have one. If you have a gmail account, you'll be able to use this. You will also need to set up a GoogleCloud project account if you haven't already, but the registration step outlined below will guide you through that.

You'll need a site that you have developer access to (duh), as well as a page that involves an action you would like to use the key on (like a submit button). The most logical use case is on a Contact form, which is subject to abuse by spam bots.

1. Registration

You'll need to register your recaptcha keys. Follow the first link on the developer site and sign in.

Follow the prompts and grab the site key and the secret key and keep them safe somewhere. Make sure you add localhost to the list of domains if you want to test this locally.

2. Client side integration (front-end)

We will now need to add reCaptcha to our site. For this example, I'm going to add the reCaptcha to a basic contact form. I've added my form_rows below so you can see the fields I'm using:

{# contact.html.twig #}
{{ form_start(form, {'attr': {'id': 'contact-form'}}) }}
   {{ form_row(form.first_name) }}
   {{ form_row(form.last_name) }}
   {{ form_row(form.email) }}
   {{ form_row(form.message) }}
    <button type="submit">
      Send Message
    </button>
{{ form_end(form) }}

I've already set up a {% block scripts %} in the <head> of my base.html.twig, so I firstly need to add the api script to this twig file:

{# contact.html.twig #}
{% extends 'base.html.twig' %}
{% block scripts %}
 {{ parent() }}
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_PUBLIC_SITE_KEY"></script>
{% endblock %}

 

We can now add the event listener to the same twig file.  It will essentially add a hidden input field to your form, and include the token needed to check the request.


<script>
  document.addEventListener('DOMContentLoaded', function() {
    grecaptcha.ready(function() {
      document.getElementById('login-form').addEventListener('submit', function(e) {
         e.preventDefault();
          grecaptcha.execute('YOUR_PUBLIC_SITE_KEY', {action: 'submit'}).then(function(token) {
          const input = document.createElement('input');
          input.type = 'hidden';
          input.name = 'g-recaptcha-response';
          input.value = token;
          e.target.appendChild(input);
          e.target.submit();
         });
       });
     });
   });
</script>

3. Server side

 For the server side, we need to add the details to a POST request to Google's site verification endpoint. This request uses the secret hey we generated in step #1, and this is best stored in the Symfony vault. See the Symfony documentation on this for details  (https://symfony.com/doc/current/configuration/secrets.html), but essentially

Generate crytoigrphic keys if you haven't already using:

php bin/console secrets:generate-keys

and then add the secret  with

php bin/console secrets:set RECAPTCHA_SECRET_KEY

Once this is set, you can refer to it using Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface.

I'm going to add the reCaptcha as a service to keep it nice and neat. To do this create a Service/RecaptchaService.php in your src/ folder. Here is mine:

//src/Service/RecaptchaService
<?php
namespace App\Service;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class RecaptchaService
{
   private string $secretKey;
   private HttpClientInterface $httpClient;
   public function __construct(ParameterBagInterface $params, HttpClientInterface $httpClient)
   {
       $this->secretKey = $params->get('RECAPTCHA_SECRET_KEY');
       $this->httpClient = $httpClient;
   }
   public function verify(string $token, ?string $userIP = null): array
   {
       try {
           //send a request to Google with the token and the secret key
           $response = $this->httpClient->request('POST', 'https://www.google.com/recaptcha/api/siteverify', [
               'body' => [
                   'secret' => $this->secretKey,
                   'response' => $token,
                   'remoteip' => $userIP
               ]
           ]);
           //Grab the results back
           $result = $response->toArray();
           //Google will return a score--you can adjust this if needed.
           //Their dashboard shows you attempts, so you can review that after
           // some traffic has passed and scale this
           if ($result['success'] && ($result['score'] ?? 0) >= 0.5) {
               return ['valid' => true, 'score' => $result['score']];
           }
           return ['valid' => false, 'score' => $result['score'] ?? 0];
       } catch (\Exception $e) {
           return ['valid' => false, 'score' => 0, 'error' => $e->getMessage()];
       }
   }
}

You can see that the request will return a result based on a  score ascribed to user actions, determining whether the actions were valid or invalid. you can adjust the score yourself.

4. Modifying  the controller.

Once we have a service to handle the request, we can use it in our controller responsible for the contact form:

<?php
namespace App\Controller;
use App\Service\RecaptchaService; //The service we jut created
use Symfony\Component\Mailer\MailerInterface; //My mailer service, yours may be different.
class ContactController extends AbstractController {
   //Controller _construct and other logic here
   //...
#[Route('/contact', methods: ['GET', 'POST'], name: 'contact')]
 public function contact(Request $request, MailerInterface $mailer, RecaptchaService $recaptchaService): Response
 {
   $form = $this->createForm(ContactFormType::class);
   $form->handleRequest($request);
   //recpatcha checks
   if ($form->isSubmitted() && $form->isValid()) {
      //We create the token from the input field we added via JS on the front-end
     $recaptchaToken = $request->request->get('g-recaptcha-response');
       //Handle no token
       if (!$recaptchaToken) {
         return $this->render('contact.html.twig', ['form' => $form->createView()]);
       }
       //here we use the reCaptch service to returna score, if it is ok, we will then
       //permit the emailing logc
       $verification = $recaptchaService->verify($recaptchaToken, $request->getClientIp());
       if ($verification['valid']) {
         $data = $form->getData();
              //your email logic goes here, taking that data and sending it.
       } else {
         //if it is not vaild, we can flash a message for the user
         $this->addFlash('error', 'Recaptcha failed.');
        ));
       }
     }
   return $this->render('contact.html.twig', [
     'form' => $form->createView()
   ]);
 }
}

There you have it. This will then enable to you have a reCaptcha working on your contact form without bothering the user.

Photo by Phillip Glickman on Unsplash