A few months ago, Iltar van der Berg commented on one of his amazing blog posts about the security problems that are in Symfony’s Security component. Well, not a security problem, but rather something that is totally unnecessary to implement while programming with the Security component.

According to Symfony’s official documentation, the entity provider page, it is a standard to bind the UserInterface to the User entity class. This results in the following result: The complete user entity is the session and you will probably also use the entity in a form. According to Iltar’s blog post, which I fully agree with, you get synchronization issues. If you update your entity, that means your session entity won’t be updated as it’s not from the database. In order to solve this issue, you can merge the entity back into the entity manager on each request.

While this solves one of the problems, another common issue is the (un)serialization. Eventually your User Entity will get relations to other objects and this comes with several side-effects: the relations will be serialized as well and if a relation is lazy loaded (standard setting), it will try to serialize the Proxy which contains a connection. This will spew some errors on your screen as the connection cannot be serialized and don’t even think about changing your Entity such as adding fields, this will cause unserialization issues with incomplete objects because of missing properties. This case is triggered for every authenticated user.

Iltar’s complete blog post explains why you should follow his method. To summarize, I will publish the source code for the sake of completeness, which will make it useful for you. The object below sole responsibility is to feed the security system with only the information required. The sole responsibility of this object is to implement the UserInterface and provide the security system with authentication:

<?php

namespace App\AccountBundle\Security;

use App\AccountBundle\Entity\Account;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Class SecurityAccount
 *
 * @author Iltar van der Berg <kjarli@gmail.com>
 * @author Ricardo de Vries <ricardo@secondpay.nl>
 */
class SecurityAccount implements UserInterface, \Serializable
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var string
     */
    private $username;

    /**
     * @var string
     */
    private $email;

    /**
     * @var string
     */
    private $password;

    /**
     * @var array
     */
    private $roles;

    /**
     * SecurityAccount constructor.
     *
     * @param Account $account
     */
    public function __construct(Account $account)
    {
        $this->id = $account->getId();
        $this->username = $account->getUsername();
        $this->email = $account->getEmail();
        $this->password = $account->getPassword();
        $this->roles = $account->getRoles();
    }

    /**
     * Removes sensitive data from the user.
     */
    public function eraseCredentials()
    {
    }

    /**
     * Returns the username used to authenticate the user.
     *
     * @return string
     */
    public function getUsername(): string
    {
        return $this->username;
    }

    /**
     * Returns the email address used to authenticate the user.
     *
     * @return string
     */
    public function getEmail(): string
    {
        return $this->email;
    }

    /**
     * Returns the password used to authenticate the user.
     *
     * @return string
     */
    public function getPassword(): string
    {
        return $this->password;
    }

    /**
     * Returns the salt that was originally used to encode the password.
     *
     * @return string|null
     */
    public function getSalt(): ?string
    {
        return null;
    }

    /**
     * Returns the roles granted to the user.
     *
     * @return array
     */
    public function getRoles(): array
    {
        return $this->roles;
    }

    /**
     * String representation of object
     *
     * @return string
     */
    public function serialize(): string
    {
        return serialize([
            $this->id,
            $this->username,
            $this->email,
            $this->password,
        ]);
    }

    /**
     * Constructs the object
     *
     * @param string $serialized
     */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->email,
            $this->password,
        ) = unserialize($serialized);
    }
}

According to the documentation, you have to implement an interface which will return an object implementing the UserInterface: the Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface. In the example this is done by adding it to your UserRepository, which is a doctrine EntityRepository. Considering your UserRepository must return User entities, you can’t simply make it return a SecurityUser. To solve this, you have to make an object using the UserRepository creating a SecurityUser:

<?php

namespace App\AccountBundle\Security;

use Doctrine\ORM\NonUniqueResultException;
use App\AccountBundle\Repository\AccountRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

/**
 * Class SecurityAccountFactory
 *
 * @author Iltar van der Berg <kjarli@gmail.com>
 * @author Ricardo de Vries <ricardo@secondpay.nl>
 */
class SecurityAccountFactory implements UserProviderInterface
{
    /**
     * @var AccountRepository
     */
    private $accountRepository;

    /**
     * SecurityAccountFactory constructor.
     *
     * @param AccountRepository $accountRepository
     */
    public function __construct(AccountRepository $accountRepository)
    {
        $this->accountRepository = $accountRepository;
    }

    /**
     * Refreshes the user for the account interface.
     *
     * @param UserInterface $user
     *
     * @return SecurityAccount
     * @throws UnsupportedUserException|UsernameNotFoundException
     */
    public function refreshUser(UserInterface $user): SecurityAccount
    {
        if (!$user instanceof SecurityAccount) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    /**
     * Loads the user for the given username.
     *
     * @param string $username
     *
     * @return SecurityAccount
     * @throws UsernameNotFoundException|NonUniqueResultException
     */
    public function loadUserByUsername($username): SecurityAccount
    {
        if (($account = $this->accountRepository->findOneByUsername($username)) === null) {
            throw new UsernameNotFoundException();
        }

        return new SecurityAccount($account);
    }

    /**
     * Whether this provider supports the given user class.
     *
     * @param string $class
     *
     * @return bool
     */
    public function supportsClass($class): bool
    {
        return SecurityAccount::class === $class;
    }
}

You will also need to add the following services to your application to make this runnable:

app.account_repository:
  class: Doctrine\ORM\EntityRepository
  factory: [ '@doctrine.orm.entity_manager', getRepository ]
  arguments:
    - 'App\AccountBundle\Entity\Account'

app.security_account:
  class: App\AccountBundle\Security\SecurityAccountFactory
  arguments:
    - '@app.account_repository'

To conclude, the entity is not longer stored in the session, avoiding synchronization and serialization issues. It also makes the entity and repository not longer tightly coupled to the security system and the SecurityAccount now only contains data required for identification.