I am new to Symfony and hope you can help me with some security/doctrine relationship issues.
I am using Symfony 6.3.1 with the security bundle and a few other bundles (saml onelogin and knpu oauth2 for authentication, not sure if this is relevant).
My project have 4 entities : CoreUser, CoreAd, User which extends CoreUser, and Ad which extends CoreAd. CoreAd has a ManyToOne relationship with User. Yeah it might be a little wonky…
When on my route app_view_advertisement, which prints Ad details, the session loses the current user somehow. After reloading the page, an exception is thrown You cannot refresh a user from the EntityUserProvider that does not contain an identifier
and the current user is logged out. This only happened after adding the ManyToOne relationship.
CoreUser Entity :
namespace CoreUserBundlesrcEntity;
use CoreUserBundlesrcEntityTypesEmailType;
use CoreUserBundlesrcRepositoryCoreUserRepository;
use SymfonyComponentSecurityCoreUserPasswordAuthenticatedUserInterface;
use SymfonyComponentSecurityCoreUserUserInterface;
use DoctrineORMMapping as ORM;
#[ORMMappedSuperclass(repositoryClass: CoreUserRepository::class)]
class CoreUser implements UserInterface, PasswordAuthenticatedUserInterface, CoreUserInterface
{
use ObjectTrait;
#[ORMId]
#[ORMGeneratedValue]
#[ORMColumn]
private ?int $id = null;
#[ORMColumn]
private array $roles = [];
/**
* @var string The hashed password
*/
#[ORMColumn]
private ?string $password = null;
#[ORMColumn(type:EmailType::class, length: 512)]
private ?string $email = null;
#[ORMColumn(length: 512, unique: true)]
private ?string $hashEmail = null;
public function __construct()
{
}
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return $this->hashEmail;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
if ($this->getAccountType() == AccountType::SysAdmin) {
$roles[] = 'ROLE_SUPER_ADMIN';
}
else{
$roles[] = 'ROLE_USER';
}
return array_unique($roles);
}
/**
* @param array $roles
* @return $this
*/
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
/**
* @param string $password
* @return $this
*/
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
/**
* @return string|null
*/
public function getEmail(): ?string
{
return $this->email;
}
/**
* @param string $email
* @return $this
*/
public function setEmail(string $email): static
{
$this->email = $email;
$this->hashEmail = EmailType::hash($email);
return $this;
}
/**
* @return string|null
*/
public function getHashEmail(): ?string
{
return $this->hashEmail;
}
/**
* @param string $hashEmail
* @return $this
*/
public function setHashEmail(string $hashEmail): static
{
$this->hashEmail = $hashEmail;
return $this;
}
}
EmailType is a custom Doctrine Type (encrypted email), hashEmail is the identifier which is the hashed email.
User Entity :
use AppRepositoryUserRepository;
use CoreUserBundlesrcEntityCoreUser;
use DoctrineORMMapping as ORM;
#[ORMEntity(repositoryClass: UserRepository::class)]
class User extends CoreUser
{
}
CodeAd Entity :
namespace CoreAdBundlesrcEntity;
use AppEntityUser;
use CoreUserBundlesrcEntityCoreUser;
use CoreUserBundlesrcEntityObjectTrait;
use DoctrineDBALTypesTypes;
use DoctrineORMMapping as ORM;
use DoctrineORMMappingJoinColumn;
use SymfonyComponentSerializerAnnotationGroups;
#[ORMMappedSuperclass]
class CoreAd
{
use ObjectTrait;
#[Groups(['extended.item'])]
#[ORMId]
#[ORMGeneratedValue]
#[ORMColumn]
private ?int $id = null;
#[ORMManyToOne(targetEntity: User::class)]
#[JoinColumn(name: 'ownerId', referencedColumnName: 'id')]
private CoreUser $owner;
public function __construct()
{
}
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return CoreUser
*/
public function getOwner(): CoreUser
{
return $this->owner;
}
/**
* @param CoreUser $owner
* @return CoreAd
*/
public function setOwner(CoreUser $owner): CoreAd
{
$this->owner = $owner;
return $this;
}
}
Ad Entity :
namespace AppEntity;
use AppRepositoryAdRepository;
use CoreAdBundlesrcEntityCoreAd;
use DoctrineORMMapping as ORM;
#[ORMEntity(repositoryClass: AdRepository::class)]
class Ad extends CoreAd
{
}
Login and logout seem to work fine, so I’m not going to copy/paste my security.yaml file or my authenticators, but please let me know if you need them as well.
The ManyToOne relationship between CoreAd and User seems misconfigured, I have an exception when viewing a page with the Ad Entity on my route app_view_advertisement, after reloading the page.
AdController :
namespace CoreAdBundlesrcController;
use AppEntityAd;
use AppRepositoryAdRepository;
use CoreAdBundlesrcFormAdFormType;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;
use SymfonyComponentSecurityHttpAttributeIsGranted;
class AdController extends AbstractController
{
#[Route('/advertisement/add/', name: 'app_add_advertisement')]
public function add(
Request $request,
AdRepository $adRepository
): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$advertisement = new Ad();
$form = $this->createForm(AdFormType::class, $advertisement);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$advertisement->setOwner($this->getUser());
$adRepository->save($advertisement, true);
}
return $this->render('@CoreAd/advertisement/add.html.twig', [
'adForm' => $form->createView()
]);
}
#[IsGranted('view', 'advertisement')]
#[Route('/advertisement/view/{id}/', name: 'app_view_advertisement')]
public function view(Ad $advertisement): Response
{
return $this->render('@CoreAd/advertisement/view.html.twig', [
'ad' => $advertisement
]);
}
}
The session seems to be losing the current user info on that route.
Without the User entity (attached the current user with $advertisement->setOwner($this->getUser());
) in the Ad Entity, it works fine.
AdRepository :
namespace AppRepository;
use AppEntityAd;
use CoreAdBundlesrcRepositoryCoreAdRepository;
use DoctrinePersistenceManagerRegistry;
class AdRepository extends CoreAdRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Ad::class);
}
}
CoreAdRepository :
namespace CoreAdBundlesrcRepository;
use CoreAdBundlesrcEntityCoreAd;
use DoctrinePersistenceManagerRegistry;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
class CoreAdRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry, string $entity = CoreAd::class)
{
parent::__construct($registry, $entity);
}
public function save(CoreAd $entity, bool $flush = false): void
{
$now = new DateTime('now');
if (is_null($entity->getCreationDate())) {
$entity->setCreationDate($now)
->setSlug($entity->slugify());
}
$entity->setLastModificationDate($now);
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(CoreAd $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}
The Exception is the following :
InvalidArgumentException:
You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.
at vendor/symfony/doctrine-bridge/Security/User/EntityUserProvider.php:88
at SymfonyBridgeDoctrineSecurityUserEntityUserProvider->refreshUser()
(vendor/symfony/security-http/Firewall/ContextListener.php:207)
at SymfonyComponentSecurityHttpFirewallContextListener->refreshUser()
(vendor/symfony/security-http/Firewall/ContextListener.php:127)
at SymfonyComponentSecurityHttpFirewallContextListener->authenticate()
(vendor/symfony/security-bundle/Debug/WrappedLazyListener.php:46)
at SymfonyBundleSecurityBundleDebugWrappedLazyListener->authenticate()
(vendor/symfony/security-bundle/Security/LazyFirewallContext.php:73)
at SymfonyBundleSecurityBundleSecurityLazyFirewallContext->SymfonyBundleSecurityBundleSecurity{closure}()
(vendor/symfony/security-core/Authentication/Token/Storage/TokenStorage.php:34)
at SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorage->getToken()
(vendor/symfony/security-core/Authentication/Token/Storage/UsageTrackingTokenStorage.php:44)
at SymfonyComponentSecurityCoreAuthenticationTokenStorageUsageTrackingTokenStorage->getToken()
(vendor/symfony/security-core/Authorization/AuthorizationChecker.php:42)
at SymfonyComponentSecurityCoreAuthorizationAuthorizationChecker->isGranted()
(vendor/symfony/security-http/EventListener/IsGrantedAttributeListener.php:65)
at SymfonyComponentSecurityHttpEventListenerIsGrantedAttributeListener->onKernelControllerArguments()
(vendor/symfony/event-dispatcher/Debug/WrappedListener.php:116)
at SymfonyComponentEventDispatcherDebugWrappedListener->__invoke()
(vendor/symfony/event-dispatcher/EventDispatcher.php:220)
at SymfonyComponentEventDispatcherEventDispatcher->callListeners()
(vendor/symfony/event-dispatcher/EventDispatcher.php:56)
at SymfonyComponentEventDispatcherEventDispatcher->dispatch()
(vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:139)
at SymfonyComponentEventDispatcherDebugTraceableEventDispatcher->dispatch()
(vendor/symfony/http-kernel/HttpKernel.php:161)
at SymfonyComponentHttpKernelHttpKernel->handleRaw()
(vendor/symfony/http-kernel/HttpKernel.php:74)
at SymfonyComponentHttpKernelHttpKernel->handle()
(vendor/symfony/http-kernel/Kernel.php:197)
at SymfonyComponentHttpKernelKernel->handle()
(vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php:35)
at SymfonyComponentRuntimeRunnerSymfonyHttpKernelRunner->run()
(vendor/autoload_runtime.php:29)
at require_once('/home/perrine/git/BoilerPlate/vendor/autoload_runtime.php')
(public/index.php:5)
Not sure how to fix this 🙁
I’m fairly new to Symfony and still learning.
Thanks for your help!
2
Answers
After a few more tests, I found a work around. After adding a serialize/unserialize method to the CoreUser Entity, the issue disappeared.
However, if anyone has an explanation for this behavior, I'm all ears!
How does your user repositories look like? Do they implement UserLoaderInterface? And what is configured as identifier field for the entity loader?
I guess that EmailType somehow disturbs the default implementation logic for loading the user entity from database. You maybe need to implement custom logic to fetch users based on their identifier in your user repository. See https://symfony.com/doc/current/security/user_providers.html#using-a-custom-query-to-load-the-user