0
votes

I have tried implementing a simple login form with both Symfony Guard and Symfony Authentication provider but despite everything I tried, both variables $last_email and $error are always empty.

I have followed this step by step: https://symfony.com/doc/4.4/security/form_login_setup.html and all of my LoginFormAuthenticator.php is identical. So is my controller and login.html.twig

public function login(AuthenticationUtils $authenticationUtils, Request $request, AuthorizationCheckerInterface $authChecker): Response
{
    // get the login error if there is one
    $error = $authenticationUtils->getLastAuthenticationError();
    dump($error);

    // last username entered by the user
    $last_email = $authenticationUtils->getLastUsername();

    return $this->render('security/login.html.twig', [
        'last_email' => $last_email,
        'error'      => $error,
    ]);
}

Here, even if the email does not exist or the password is invalid, $error is always null. Why?

Here are the logs:

[2020-08-23 11:53:35] request.INFO: Matched route "login". {"route":"login","route_parameters":{"_route":"login","_controller":"App\Controller\SecurityController::login"},"request_uri":"http://localhost:8000/login","method":"POST"} [2020-08-23 11:53:35] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} [2020-08-23 11:53:35] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\Security\LoginFormAuthenticator"} [2020-08-23 11:53:35] security.DEBUG: Calling getCredentials() on guard authenticator. {"firewall_key":"main","authenticator":"App\Security\LoginFormAuthenticator"} [2020-08-23 11:53:35] security.DEBUG: Passing guard token information to the GuardAuthenticationProvider {"firewall_key":"main","authenticator":"App\Security\LoginFormAuthenticator"} [2020-08-23 11:53:35] security.INFO: Guard authentication failed. {"exception":"[object] (Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException(code: 0): at /src/Security/LoginFormAuthenticator.php:68)","authenticator":"App\Security\LoginFormAuthenticator"} [2020-08-23 11:53:35] security.DEBUG: The "App\Security\LoginFormAuthenticator" authenticator set the response. Any later authenticator will not be called {"authenticator":"App\Security\LoginFormAuthenticator"} [2020-08-23 11:53:35] request.INFO: Matched route "login". {"route":"login","route_parameters":{"_route":"login","_controller":"App\Controller\SecurityController::login"},"request_uri":"http://localhost:8000/login","method":"GET"} [2020-08-23 11:53:35] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} [2020-08-23 11:53:35] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\Security\LoginFormAuthenticator"} [2020-08-23 11:53:35] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\Security\LoginFormAuthenticator"} [2020-08-23 11:53:35] security.INFO: Populated the TokenStorage with an anonymous Token.

My Security.yaml

security:
    access_denied_url: /
    encoders:
        App\Entity\User:
            algorithm: bcrypt
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        in_memory: { memory: ~ }
        our_db_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            pattern:    ^/
            http_basic: ~
            provider: our_db_provider
            anonymous: ~
            form_login:
                use_referer: true
                csrf_token_generator: security.csrf.token_manager
            remember_me:
                secret:   '%kernel.secret%'
                lifetime: 604800 # 1 week in seconds
                path:     /
                name:     REMEMBERME
                remember_me_parameter: _remember_me
            logout:
                path:  /logout
                target: /
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
    # activate different ways to authenticate

    # activate different ways to authenticate
    # https://symfony.com/doc/current/security.html#firewalls-authentication

    # https://symfony.com/doc/current/security/impersonating_user.html
    # switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    role_hierarchy:
        ROLE_ADMIN:    [ROLE_USER, ROLE_INVESTOR, ROLE_STARTUP]
        ROLE_INVESTOR: [ROLE_USER]
        ROLE_STARTUP:  [ROLE_USER]
    access_control:
        - { path: ^/login$,             roles: IS_AUTHENTICATED_ANONYMOUSLY }

My LoginFormAuthenticator.php

class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
    use TargetPathTrait;

    public const LOGIN_ROUTE = 'login';

    private $entityManager;
    private $urlGenerator;
    private $csrfTokenManager;
    private $passwordEncoder;
    private $flash;

    public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder, FlashBagInterface $flash)
    {
        $this->entityManager = $entityManager;
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
        $this->flash = $flash;
    }

    public function supports(Request $request)
    {
        return self::LOGIN_ROUTE === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        $credentials = [
            'email' => $request->request->get('email'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];

        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['email']
        );

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
//            throw new InvalidCsrfTokenException();
        }

        $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
        dump($user);
        if (!$user) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('Email could not be found.');
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

         return new RedirectResponse($this->urlGenerator->generate('logged_in'));
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate(self::LOGIN_ROUTE);
    }
}

Any help is much appreciated. I'm using Symfony 4.4

EDIT: So the login form WORKS - I can log in and log out. But if my email doesn't exist or if my credentials are invalid, no error message get displayed. Doing a var_dump on $error and $last_email returns NULL and empty string, always. Also, I tried to var dump $this->passwordEncoder->isPasswordValid($user, $credentials['password']); from my LoginFormAuthenticator in the checkCredentials method and if my credentials are invalid, i can see it returns false. So that works. But somehow, something seems broken in the background.

1
Start by commenting out the redirect line at the top of your code. I suspect you have a double redirect going on and you never even get to the $error = line. And replace var_dump with Symfony's dump. - Cerad
You have twice Matched route "login" at the same time. That can be a problem but not the solution. You should go deeper AuthenticationUtils to debug and get more information about where the problem can be. - S.LT
$authenticationUtils->getLastAuthenticationError(); returns the last exception thrown in your GuardAuthenticator. So you either not throw any exception or the redirect makes you lose the data - TZiebura
Thank you. I have removed the redirect for test purposes but still have the same issue. @S.LT what do you mean go deeper in AuthenticationUtils? Do you mean debug the function? - Miles M.
Yes @MilesM., why Guard authenticator does not support the request? - S.LT

1 Answers

0
votes

From different comments under your question, I think you should take a look at the documentation, on each step to check if you haven't forgotten something. Guard authentification

For the missing error message, you can throw a custom Exception: Guard customize error

I see you have not an onAuthenticationFailure inside your LoginFormAuthenticator.

onAuthenticationFailure(Request $request, AuthenticationException $exception) This is called if authentication fails. Your job is to return the Symfony\Component\HttpFoundation\Response object that should be sent to the client. The $exception will tell you what went wrong during authentication.

You can add one to do an action on authentication failure.