Search code examples
symfonysymfony4

No error and no user in session after login form is validated in Symfony 4


I'm trying to make a simple login form in Symfony 4 (followed this documentation: https://symfony.com/doc/current/security/form_login_setup.html).

I have a user in database. If I enter wrong credentials in the login form, I get wrong credentials error messages. But if I enter right credentials I get redirected to home (default behaviour) but no sessions is started, the user is not in session.

This is the SecurityController I'm using:

<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends Controller
{
    /**
     * @Route("/login", name="login")
     */
    public function login(Request $request, AuthenticationUtils $authenticationUtils)
    {
        $error = $authenticationUtils->getLastAuthenticationError();

        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('security/login.html.twig', array(
            'last_username' => $lastUsername,
            'error'         => $error,
        ));
    }
}

The security.yaml file:

security:
encoders:
    App\Entity\user:
        algorithm: bcrypt

providers:
    in_memory: { memory: ~ }
    quimly_db_provider:
        entity:
            class: App\Entity\User
            property: username

firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false

    main:
        anonymous: ~

        pattern:    ^/
        http_basic: ~
        provider: quimly_db_provider
        form_login:
            login_path: login
            check_path: login

The twig:

 {% extends 'base.html.twig' %}

{% block body %}

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

<form action="{{ path('login') }}" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="_username" value="{{ last_username }}" />

    <label for="password">Password:</label>
    <input type="password" id="password" name="_password" />

    <button type="submit">login</button>
</form>

{% endblock %}

I have read this question: How to create a login authentification form with symfony 4

But it doesn't solve the problem.

If anyone has any knowledge about this, help would be very much appreciated !

Cheers.


Solution

  • There is an issue with something called the EquatableInterface that causes this sort of problems. To be honest, I don't understand fully why it does what it does. But it's something in the authentication process which checks to see if the user has somehow changed between requests.

    Update: Assume an administrator reset's a user's password while the user is logged in. If you don't check for a changed password then the user can continue to work with the old password. It is up to you to decide which behavior you want. Serializing the password and using the default EquatableInterface implementation will force the user to log in again.

    One way to avoid the issue is to serialize the password.

    // App/Entity/User.php
    public function serialize()
    {
        //die('serialize');
        return serialize(array(
            $this->id,
            $this->username,
            $this->password
        ));
    }
    
    public function unserialize( $serialized )
    {
        list (
            $this->id,
            $this->username,
            $this->password
            ) = unserialize($serialized, ['allowed_classes' => false]);
    }
    

    Fix your User::getRoles and it should work.

    The other approach is to implement the EquatableInterface

    class User implements UserInterface, \Serializable, EquatableInterface
    
    public function isEqualTo(UserInterface $user)
    {
        //if ($this->password !== $user->getPassword()) {
        //    return false;
        //}
    
        //if ($this->salt !== $user->getSalt()) {
        //    return false;
        //}
    
        if ($this->username !== $user->getUsername()) {
            return false;
        }
    
        return true;
    }
    

    With this approach there is no need to serialize the password. Either way seems to work fine.