symfonysymfony4jmsserializerbundle

Symfony authentication : Why user password is not serialized in TokenInterface


I configured a firewall to protect part of my application (/manager). I also setup a login form to authenticate, but i loop on login form.

I'm sure password is the right one, and after some investigation i found that the authentication worked well (i was authenticated) but right after it, when symfony refresh the user, it log me out cause the user is not the same. I found that was caused by password mismatch. The problem is that when i'm logged in by authenticator, User is serialised (with TokenInterface i guess), but the field 'password' is set to null in TokenInterface (not in database). I'm using JMSSerialiserBundle in my application, i don't know if the problem comes to it. Can you help me ?

My user class

/**
 * @ORM\Entity(repositoryClass="App\Repository\UtilisateurRepository")
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="type", type="string")
 * @ORM\DiscriminatorMap({"eleve" = "Eleve", "gestionnaire" = "Gestionnaire"})
 */
abstract class Utilisateur implements UserInterface
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @Serializer\Groups({"id"})
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Serializer\Groups({"Utilisateur"})
     */
    private $nom;

    /**
     * @ORM\Column(type="string", length=255)
     * @Serializer\Groups({"Utilisateur"})
     */
    private $prenom;

    /**
     * @ORM\Column(type="string", length=255)
     * @Serializer\Groups({"Utilisateur"})
     */
    private $username;

    /**
     * @ORM\Column(type="string", length=255)
     * @Serializer\Exclude()
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=1024, nullable=true)
     * @Serializer\Groups({"Login"})
     */
    private $token;

My Authenticator class

class ManagerAuthenticator extends AbstractFormLoginAuthenticator
{
    use TargetPathTrait;

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

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

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

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

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

        return $credentials;
    }

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

        $user = $userProvider->loadUserByUsername($credentials['username']);

        if (!$user) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('Username could not be found.');
        }

        return $user;
    }

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

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        //On redirige sur la page précédente sauf si on vient du login
        $targetPath = $this->getTargetPath($request->getSession(), $providerKey);
        $loginRoute = $this->urlGenerator->generate('app_login');

        if ($targetPath && strpos($loginRoute, $targetPath) === false) {
            return new RedirectResponse($targetPath);
        }

        //Rediriger vers l'index
        return new RedirectResponse($this->urlGenerator->generate('accueil_index'));
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate('app_login');
    }
}

Even without @Serializer\Exclude() it doesn't work. I don't know if i'm supposed to override some function or implement interface.


Solution

  • Check Symfony\Component\Security\Core\Authentication\Token\AbstractToken::hasUserChanged()

    My advice create new method on your user class with name isEqualTo and your class should implement EquatableInterface.

    /**
     * @param UserInterface $user
     * @return bool
     */
    public function isEqualTo(UserInterface $user): bool
    {
        return $this->username === $user->getUsername() && $this->id === $user->getId();
    }
    

    Also you may need to add this two methods.

    /**
     * @see \Serializable::serialize()
     */
    public function serialize(): string
    {
        return serialize([$this->id, $this->username,]);
    }
    
    /**
     * @see \Serializable::unserialize()
     * @param string $serialized
     * @return User
     */
    public function unserialize($serialized): self
    {
        [$this->id, $this->username] = unserialize($serialized);
    
        return $this;
    }