FriendsOfSymfony / FOSUserBundle

Provides user management for your Symfony project. Compatible with Doctrine ORM & ODM, and custom storages.
https://symfony.com/doc/master/bundles/FOSUserBundle/index.html
MIT License
3.25k stars 1.57k forks source link

Add Captcha to Login Form #1406

Closed josecelano closed 10 years ago

josecelano commented 10 years ago

I am trying to add a Captcha to Login form whithout patching Symfony core and using FOSUserBundle.

I do not find any class in FOSUserBundle where to check the captcha.

I have added the captcha to the form template but when I do the submit the captcha is not validated.

I use my own LoginFormType.

stof commented 10 years ago

FOSUserBundle is not responsible for handling the login form. The authentication is provided by SecurtiyBundle, nit by FOSUserBundle

josecelano commented 10 years ago

Then, could I do this:

http://symfony.com/doc/current/cookbook/security/custom_password_authenticator.html

with FOSUserBundle whithout compatibility problems?

Dou you have any idea how can I do that without patching SecuriteBundle?

stof commented 10 years ago

FOSUserBundle is about managing users. It does not interfer with the authentication layer

gauravg47 commented 9 years ago

Hi, Use this: How To Insert Symfony2 Captcha To Login Page In FOSUserBundle http://webmuch.com/how-to-insert-symfony2-captcha-to-login-page-in-fosuserbundle/

josecelano commented 9 years ago

Thanks @gauravg47 . I did somethink like this: http://stackoverflow.com/questions/14788828/adding-captcha-to-symfony2-login-page but I am goging to change it and do my own authentication controller as it is in your link. I think it is a better solution (less coupled to Symfony).

malutanpetronel commented 9 years ago

did it worked josecelano ?

tuanalumi commented 6 years ago

I have made this work by overriding the UsernamePasswordFormAuthenticationListener class of SecurityBundle (Symfony 3.4 + FOSUserBundle 2.x)

dkarlovi commented 6 years ago

@tuanalumi how exactly did you do it? I got the recaptcha in form using EWZRecaptchaBundle, but having trouble figuring out how to validate it in attemptAuthentication.

dkarlovi commented 6 years ago

Nevermind, I found a way, just not a very nice one:

        <!-- services.xml -->
        <service id="UserBundle\Security\Listener\UsernamePasswordFormAuthenticationListener"
                 decorates="security.authentication.listener.form.main">
            <argument key="$authenticationListener" type="service" id="UserBundle\Security\Listener\UsernamePasswordFormAuthenticationListener.inner"/>
            <argument key="$httpUtils" type="service" id="security.http_utils"/>
            <argument key="$failureHandler" type="service" id="security.authentication.failure_handler.main.form_login"/>
        </service>
<?php

declare(strict_types=1);

namespace UserBundle\Security\Listener;

use EWZ\Bundle\RecaptchaBundle\Validator\Constraints\IsTrue as IsValidRecaptcha;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener as BaseListener;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use UserBundle\Service\FailureCounterService;

/**
 * Class UsernamePasswordFormAuthenticationListener.
 */
class UsernamePasswordFormAuthenticationListener implements ListenerInterface
{
    /**
     * @var ValidatorInterface
     */
    private $validator;

    /**
     * @var FailureCounterService
     */
    private $failureCounterService;

    /**
     * @var BaseListener
     */
    private $authenticationListener;

    /**
     * @var HttpUtils
     */
    private $httpUtils;

    /**
     * @var AuthenticationFailureHandlerInterface
     */
    private $failureHandler;

    public function __construct(
        ValidatorInterface $validator,
        FailureCounterService $failureCounterService,
        BaseListener $authenticationListener,
        HttpUtils $httpUtils,
        AuthenticationFailureHandlerInterface $failureHandler
    ) {
        $this->validator = $validator;
        $this->failureCounterService = $failureCounterService;
        $this->authenticationListener = $authenticationListener;
        $this->httpUtils = $httpUtils;
        $this->failureHandler = $failureHandler;
    }

    /**
     * @param GetResponseEvent $event
     */
    public function handle(GetResponseEvent $event): void
    {
        $request = $event->getRequest();

        // TODO: hardcoded options, extract from container
        if (false === $request->isMethod('POST') && false === $this->httpUtils->checkRequestPath($request, '/login_check')) {
            return;
        }

        if ($this->failureCounterService->isHardThresholdFailureCountReached('login')) {
            // hard lock, login denied
            $this->setExceptionResponse($event, $request, new LockedException());

            return;
        }

        if ($this->failureCounterService->isSoftThresholdFailureCountReached('login')) {
            // soft lock, a valid captcha is required
            $resp = $this->validator->validate(null, new IsValidRecaptcha());
            if ($resp->count() > 0) {
                $this->setExceptionResponse($event, $request, new CustomUserMessageAuthenticationException('security.captcha.required'));

                return;
            }
        }

        $this->authenticationListener->handle($event);
    }

    /**
     * @param GetResponseEvent        $event
     * @param Request                 $request
     * @param AuthenticationException $exception
     */
    private function setExceptionResponse(GetResponseEvent $event, Request $request, AuthenticationException $exception): void
    {
        $event->setResponse($this->failureHandler->onAuthenticationFailure($request, $exception));
    }
}
tuanalumi commented 6 years ago

@dkarlovi I did something like below. Note: I override the login form and use javascript to add reCaptcha (no extra bundle).

// file: src/UserBundle/SecurityListener/FormAuthenticationListener.php

namespace UserBundle\SecurityListener;

use GuzzleHttp\Client;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener;
use UserBundle\Exception\InvalidReCaptchaException;

class FormAuthenticationListener extends UsernamePasswordFormAuthenticationListener
{
    public function attemptAuthentication(Request $request)
    {
        if (!$this->verifyRecaptcha($request->request->get('g-recaptcha-response'))) {
            throw new InvalidReCaptchaException();
        }

        return parent::attemptAuthentication($request);
    }

    private function verifyRecaptcha($recaptchaResponse)
    {
        $client = new Client();

        $response = $client->post(
            'https://www.google.com/recaptcha/api/siteverify',
            [
                'form_params' => [
                    'secret'   => 'lol, nice try',
                    'response' => $recaptchaResponse,
                ]
            ]
        );

        $result = json_decode($response->getBody(), true);

        return !empty($result['success']);
    }
}
//file: src/UserBundle/DependencyInjection/Compiler/OverrideServiceCompilerPass.php

namespace UserBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use UserBundle\Controller\SecurityController;
use UserBundle\SecurityListener\FormAuthenticationListener;

class OverrideServiceCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $container
            ->getDefinition('security.authentication.listener.form')
            ->setClass(FormAuthenticationListener::class)
        ;

        $container
            ->getDefinition('fos_user.security.controller')
            ->setClass(SecurityController::class)
        ;
    }
}