Symfony 4 FOSUserBundle CAPTCHA Code Example

The FOSUserBundle adds support for a database-backed user system in Symfony. It provides a flexible framework for user management that aims to handle common tasks such as user registration and password retrieval. This code example shows you how to integrate CaptchaBundle into FOSUserBundle login and register forms.

First Time Here?

Check the BotDetect Symfony 4 Captcha Quickstart for key integration steps.

Alongside the Captcha image, the user is provided with an input field to retype the displayed characters, and a message stating the Captcha code validation result which is displayed after form submission.

This example requires the FOSUserBundle installed in your Symfony application. Follow these steps to install FOSUserBundle to Symfony app/site if you haven't already.

Files for this example are:

Config - /config/packages/captcha.php

<?php if (!class_exists('CaptchaConfiguration')) { return; }

// BotDetect PHP Captcha configuration options

return [
  // Captcha configuration for login page
  'LoginCaptcha' => [
    'UserInputID' => 'captchaCode',
    'CodeLength' => CaptchaRandomization::GetRandomCodeLength(4, 6),
    'ImageStyle' => [
      ImageStyle::Radar,
      ImageStyle::Collage,
      ImageStyle::Fingerprints,
    ],
  ],

  // Captcha configuration for register page
  'RegisterCaptcha' => [
    'UserInputID' => 'captchaCode',
    'CodeLength' => CaptchaRandomization::GetRandomCodeLength(4, 7),
    'CodeStyle' => CodeStyle::Alpha,
  ],

];

In order to use the CaptchaBundle, we have defined Captcha configuration which will be used as a CaptchaType in RegistrationFormType form and to get a captcha object instance in Login Controller. Check BotDetect Symfony 4 integration guide for details.

Form Type - /src/Form/Type/RegistrationFormType.php

<?php 

namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
// Importing the CaptchaType class
use Captcha\Bundle\CaptchaBundle\Form\Type\CaptchaType;

class RegistrationFormType extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    parent::buildForm($builder, $options);

    $builder->add('captchaCode', CaptchaType::class, array(
      'captchaConfig' => 'RegisterCaptcha',
      'label' => 'Retype the characters from the picture'
    ));
  }

  public function getParent()
  {
    return 'FOS\UserBundle\Form\Type\RegistrationFormType';
  }

  public function getBlockPrefix()
  {
    return 'app_user_registration';
  }
}

The RegistrationFormType inherits from the base FOSUserBundle fos_user_registration type and then it uses CaptchaType to add Captcha in register form. It is required to declare captchaConfig option and assigns it a captcha configuration key defined in app/config/captcha.php file (i.e. RegisterCaptcha). You're able to check here in order to know more about overriding a Form Type in FOSUserBundle.

Entity – /src/Entity/User.php

<?php 

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use Captcha\Bundle\CaptchaBundle\Validator\Constraints as CaptchaAssert;

/**
 * @ORM\Entity
 * @ORM\Table(name="fos_user")
 */
class User extends BaseUser
{
  /**
   * @ORM\Id
   * @ORM\Column(type="integer")
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  protected $id;

  /**
   * @CaptchaAssert\ValidCaptcha(
   *      message = "CAPTCHA validation failed, try again."
   * )
   */
  protected $captchaCode;

  public function getCaptchaCode()
  {
    return $this->captchaCode;
  }

  public function setCaptchaCode($captchaCode)
  {
    $this->captchaCode = $captchaCode;
  }
  public function __construct()
  {
    parent::__construct();
    // your own logic
  }
}

To validate the captchaCode field in contact form, we have added the ValidCaptcha constraint to User Entity.

View – /templates/ FOSUserBundle/Security/login_content.html.twig

{% trans_default_domain 'FOSUserBundle' %}

<h2>Login Form Validation BotDetect CAPTCHA Example</h2>

{% block stylesheets %}
  <link href="{{ asset('assets/css/style.css') }}" rel="stylesheet" />
{% endblock %}

{% if error %}
  <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}

<form action="./login" method="post">
  {% if csrf_token %}
    <input type="hidden" name="_csrf_token" value="{{ csrf_token }}" />
  {% endif %}

  <label for="username">{{ 'security.login.username'|trans }}</label>
  <input type="text" id="username" name="_username" value="{{ last_username }}" required="required" autocomplete="username" />

  <label for="password">{{ 'security.login.password'|trans }}</label>
  <input type="password" id="password" name="_password" required="required" autocomplete="current-password" />

  {# show captcha image  #}
  {{ captcha_html | raw  }}
  <input type="text" id="captchaCode" name="captchaCode">

  <input type="checkbox" id="remember_me" name="_remember_me" value="on" />
  <label for="remember_me">{{ 'security.login.remember_me'|trans }}</label>

  <input type="submit" id="_submit" name="_submit" value="{{ 'security.login.submit'|trans }}" />
</form>

<div style="margin-top:15px">
  <ul >
    <li><a href="./register">Register Form CAPTCHA Example</a></li>
  </ul>
</div>

The above code uses Twig syntax to generate Captcha image. The form must contain an input field of your choice in which user will retype characters from Captcha challenge. This user-entered code should be available to you in Controller code after form submission.

The form action points to Controller action of the View it belongs to (by default, it uses the fos_user_security_check route). Also, the name of the input field corresponds to the variable in the request object that we will use for Captcha validation in the Controller.

The Captcha markup made available in the Controller is used in the View to compose a simple form with one input field and a Captcha image.

Controller – /src/Controller/SecurityController.php

<?php

namespace App\Controller;

use Captcha\Bundle\CaptchaBundle\Security\Core\Exception\InvalidCaptchaException;
use FOS\UserBundle\Controller\SecurityController as BaseController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

class SecurityController extends BaseController
{
  private $tokenManager;

  public function __construct(CsrfTokenManagerInterface $tokenManager = null)
  {
    $this->tokenManager = $tokenManager;
  }

  public function loginAction(Request $request)
  {
    /** @var $session Session */
    $session = $request->getSession();

    $authErrorKey = Security::AUTHENTICATION_ERROR;
    $lastUsernameKey = Security::LAST_USERNAME;

    // get captcha object instance
    $captcha = $this->get('captcha')->setConfig('LoginCaptcha');

    if ($request->isMethod('POST')) {
      // validate the user-entered Captcha code when the form is submitted
      $captchaCode = $request->request->get('captchaCode');
      $isHuman = $captcha->Validate($captchaCode);
      if ($isHuman) {
        // Captcha validation passed, check username and password
        return $this->redirect($this->generateUrl('fos_user_security_check'), 307);
      } else {
        // Captcha validation failed, set an invalid captcha exception in $authErrorKey attribute
        $invalidCaptchaEx = new InvalidCaptchaException('CAPTCHA validation failed, try again.');
        $request->attributes->set($authErrorKey, $invalidCaptchaEx);

        // set last username entered by the user
        $username = $request->request->get('_username', null, true);
        $request->getSession()->set($lastUsernameKey, $username);
      }
    }

    // get the error if any (works with forward and redirect -- see below)
    if ($request->attributes->has($authErrorKey)) {
      $error = $request->attributes->get($authErrorKey);
    } elseif (null !== $session && $session->has($authErrorKey)) {
      $error = $session->get($authErrorKey);
      $session->remove($authErrorKey);
    } else {
      $error = null;
    }

    if (!$error instanceof AuthenticationException) {
      $error = null; // The value does not come from the security component.
    }

    // last username entered by the user
    $lastUsername = (null === $session) ? '' : $session->get($lastUsernameKey);

    $csrfToken = $this->tokenManager
      ? $this->tokenManager->getToken('authenticate')->getValue()
      : null;

    return $this->renderLogin(array(
      'last_username' => $lastUsername,
      'error' => $error,
      'csrf_token' => $csrfToken,
      'captcha_html' => $captcha->Html()
    ));
  }
}

By default, the Login form of FOSUserBundle uses <form> tag instead of Form type as Register form, and we can not use CaptchaType in it. But we can override the SecurityController of FOSUserBundle and easily integrate Captcha in a Login form.

In the code above, the first step is to get a captcha object instance by calling the captcha service and then pass it a Login form's Captcha configuration variable defined in config/packages/captcha.php file (i.e. LoginCaptcha).

Later (at the bottom of the code snippet given above), we pass the Html required to render Captcha challenge to View.

Validation needs to be performed in the loginAction(), on HTTP POST request -- which occurs when user submits the form. To validate the user's Captcha code input, we called the Validate() method of the $captcha object. On captcha validation success, we redirected page to fos_user_security_check route to execute FOSUserBundle verification steps. On failure, we created an AuthenticationException setting an $authErrorKey attribute to CaptchaBundle's InvalidCaptchaException.

Services - /config/services.yaml

services:
    App\Controller\SecurityController:
        decorates: 'fos_user.security.controller'
        arguments: ['@security.csrf.token_manager']

Declare a decorated service as above code in order to replace FOSUserBundle's SecurityController with the custom App\Controller\SecurityController above.