Search code examples
symfonyvalidationauthenticationrecaptchasymfony-3.3

What is the right way to implement recaptcha in symfony 3 login?


I am using form_login in two different firewalls, one for user, one for admin. I want this to only affect the user one.

What would be the right implementation to have recaptcha on login form?

Some things I'm considering:

  • New login form factory which extends the symfony FormLoginFactory, where I can validate the recaptcha or
  • Overriding the UsernamePasswordFormAuthenticationListener so that form_login uses a new one which validates captcha or
  • Having the captcha on its own page and show it only when user enters invalid credentials a number of times

Solution

  • I created a bundle for this question: https://packagist.org/packages/syspay/login-recaptcha-bundle

    Old response:

    What I did to solve this problem:

    I created a new Security Listener Factory called CaptchaLoginFormFactory which has the following

    <?php
    
    namespace Project\Bundle\CoreBundle\DependencyInjection\Security\Factory;
    
    use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory;
    
    /**
     * CaptchaLoginFormFactory
     */
    class CaptchaLoginFormFactory extends FormLoginFactory
    {
        /**
         * {@inheritdoc}
         */
        public function getKey()
        {
            return 'form_login_captcha';
        }
    
        /**
         * {@inheritdoc}
         */
        protected function getListenerId()
        {
            return 'security.authentication.listener.form_login_captcha';
        }
    }
    

    and a new Authentication Listener called CaptchaFormAuthenticationListener

    <?php
    
    namespace Project\Bundle\CoreBundle\Security\Firewall;
    
    use Project\Security\CaptchaManager;
    use Project\Security\Exception\InvalidCaptchaException;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener;
    use Symfony\Component\Security\Http\ParameterBagUtils;
    
    /**
     * CaptchaFormAuthenticationListener
     */
    class CaptchaFormAuthenticationListener extends UsernamePasswordFormAuthenticationListener
    {
        /** @var CaptchaManager $captchaManager */
        private $captchaManager;
    
        /**
         * setCaptchaManager
         *
         * @param CaptchaManager $captchaManager
         */
        public function setCaptchaManager(CaptchaManager $captchaManager)
        {
            $this->captchaManager = $captchaManager;
        }
    
        /**
         * {@inheritdoc}
         */
        protected function attemptAuthentication(Request $request)
        {
            if ($this->captchaManager->isCaptchaNeeded($request)) {
                $requestBag = $this->options['post_only'] ? $request->request : $request;
                $recaptchaResponse = ParameterBagUtils::getParameterBagValue($requestBag, 'g-recaptcha-response');
    
                if (!$this->captchaManager->isValidCaptchaResponse($recaptchaResponse, $request->getClientIp())) {
                    throw new InvalidCaptchaException();
                }
            }
    
            return parent::attemptAuthentication($request);
        }
    }
    

    As can be seen they extend the original FormFactory with some changes where before I use the normal authentication listener I use my own methods to validate the captcha.

    Then I added it in the CoreBundle::build method

    public function build(ContainerBuilder $container)
    {
        parent::build($container);
    
        $extension = $container->getExtension('security');
        $extension->addSecurityListenerFactory(new CaptchaLoginFormFactory());
    }
    

    and created the services

    security.authentication.listener.form_login_captcha:
        class: Project\Bundle\CoreBundle\Security\Firewall\CaptchaFormAuthenticationListener
        parent: security.authentication.listener.form
        abstract: true
        calls:
            - [ setCaptchaManager, ['@project.security.captcha_manager'] ]
    

    Then in security.yml under the firewall I just use the new factory form_login_captcha with the same options as form_login. This way I can use form_login on another firewall without affecting it at all.