Symfony 2 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 Symfony2 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 ('bd-captcha-symfony2-examples') example are:

The files are available for download as a part of the BotDetect Captcha Symfony integration package.

Config - /app/config/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 captcha field type in RegistrationFormType form and to get a captcha object instance in Login Controller. Check BotDetect Symfony2 integration guide for details.

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

<?php namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

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

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

  public function getParent()
  {
    return 'fos_user_registration';
  }

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

We declared custom form type as a service and added tag to it. An alias value app_user_registration is similar with the string returned form getName() method of RegistrationFormType.

The RegistrationFormType inherits from the base FOSUserBundle fos_user_registration type and then it uses the captcha field type 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).

Entity – /src/AppBundle/Entity/User.php

<?php namespace AppBundle\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 – /src/AppBundle/Resources/views/Security/login.html.twig

{% extends "FOSUserBundle::layout.html.twig" %}

{% trans_default_domain 'FOSUserBundle' %}

{% block title %}Login Form Validation BotDetect CAPTCHA Example{% endblock %}
{% block stylesheets %}
  <link href="{{ path('captcha_layout_stylesheet_url') }}" rel="stylesheet" />
{% endblock %}

{% block fos_user_content %}
  <h2>Login Form Validation BotDetect CAPTCHA Example</h2>

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

  <form method="post">
    <input type="hidden" name="_csrf_token" value="{{ csrf_token }}" />

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

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

    {# 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>
{% endblock fos_user_content %}

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. The View also adds BotDetect's stylesheet using the captcha_layout_stylesheet_url route.

Controller – /src/AppBundle/Controller/SecurityController.php

<?php namespace AppBundle\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;

class SecurityController extends BaseController
{
  public function loginAction(Request $request)
  {
    /** @var $session \Symfony\Component\HttpFoundation\Session\Session */
    $session = $request->getSession();

    if (class_exists('\Symfony\Component\Security\Core\Security')) {
      $authErrorKey = Security::AUTHENTICATION_ERROR;
      $lastUsernameKey = Security::LAST_USERNAME;
    } else {
      // BC for SF < 2.6
      $authErrorKey = SecurityContextInterface::AUTHENTICATION_ERROR;
      $lastUsernameKey = SecurityContextInterface::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
      $code = $request->request->get('captchaCode');
      $isHuman = $captcha->Validate($code);
      if ($isHuman) {
        // Captcha validation passed, check username and password
        return $this->redirectToRoute('fos_user_security_check', [
          'request' => $request], 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);

    if ($this->has('security.csrf.token_manager')) {
      $csrfToken = $this->get('security.csrf.token_manager')->getToken('authenticate')->getValue();
    } else {
      // BC for SF < 2.4
      $csrfToken = $this->has('form.csrf_provider')
        ? $this->get('form.csrf_provider')->generateCsrfToken('authenticate')
        : 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 the captcha field type 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 app/config/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.

Bundle - /src/AppBundle/AppBundle.php

#src\AppBundle\AppBundle.php
namespace AppBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AppBundle extends Bundle
{
  public function getParent()
  {
    return 'FOSUserBundle';
  }
}

Finally, in order to FOSUserBundle to be able to understand that we are overriding its Controller, we need to have a child bundle whose parent is FOSUserBundle. The above code snippet has been created a bundle named AppBundle that declares itself a child of FOSUserBundle.