Hi,
I'm sorry that I'm posting this question here, as an issue, but I have a problem what I wanna solve nicely, and I guess you can help me the most.
So, I would like to use your database-less user provider solution, with my own User entity, to save at least one extra database query (getting user).
AppBundle\EntityUser:
/**
* @ORM\Entity
*/
class User implements UserInterface, JWTUserInterface
{
public function __construct(array $payload = [])
{
foreach ($payload as $key => $val) {
if (property_exists($this, $key)) {
$this->{$key} = $val;
}
}
}
public static function createFromPayload($username, array $payload)
{
return new self($payload);
}
}
security.yml:
security:
providers:
db_custom:
id: app.user_provider
jwt:
lexik_jwt:
class: AppBundle\Entity\User
firewalls:
login:
pattern: ^/login
anonymous: true
stateless: true
form_login:
check_path: api_login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
provider: db_custom
api:
pattern: ^/
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
provider: jwt
app.user_provider:
class UserProvider implements UserProviderInterface
{
private $entityManager;
private $class;
public function __construct(EntityManagerInterface $entityManager, $class)
{
$this->entityManager = $entityManager;
$this->class = $class;
}
public function loadUserByUsername($username)
{
$user = $this->entityManager->getRepository(User::class)->loadUserByUsername($username);
if ($user !== null) {
return $user;
}
throw new UsernameNotFoundException(sprintf("Unable to find user identified by '%s'.", $username));
}
public function refreshUser(SymfonyUserInterface $user)
{
$class = get_class($user);
if ($this->supportsClass($class)) {
return $this->loadUserByUsername($user->getUsername());
}
throw new UnsupportedUserException(sprintf("Instances of '%s' are not supported.", $class));
}
public function supportsClass($class)
{
return $class === $this->class || (new \ReflectionClass($class))->implementsInterface(UserInterface::class);
}
}
JWTCreatedListener:
class JWTCreatedListener
{
public function onJWTCreated(JWTCreatedEvent $event)
{
$payload = $event->getData();
$user = $event->getUser();
if ($user instanceof UserInterface) {
$payload['id'] = $user->getId();
$payload['email'] = $user->getEmail();
$payload['roles'] = $user->getRoles();
$event->setData($payload);
}
}
}
Until I try to insert a new entity with a REST client for example, everything works fine. And then I get the following ORMInvalidArgumentException:
A new entity was found through the relationship 'AppBundle\Entity\Video#createdBy' that was not configured to cascade persist operations for entity: AppBundle\Entity\User. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'AppBundle\Entity\User#__toString()' to get a clue.
(The createdBy property was set by a Doctrine EventSubscriber.)
Now, in my opinion the issue is that the JWTUserToken contains a User instance which is detached from the current EntityManager.
I thought that I could (re)attach/merge it on JWTAuthenticated event, but it didn't work because it was never managed, so merge is equivalent to persist.
If I put the serialized entity under a payload key, and unserialized it in the createFromPayload method, it could be successfully merged, but the size of token got to be much bigger, and I got back the unwished extra query (getting user). And neither of these ideas looks good.
Have you a better idea, or a best practice?
Thanks.
Hi @rolandrajko,
I'm not able to say what happen right now, I'll try to reproduce this weekend.
However, I would recommend to not use a doctrine entity and keep using a different model than the one managed by doctrine, first to avoid such issues (I have to look at the exact problem though), but especially because coupling your domain with 3rd party bundles/libraries is not ideal (see http://elnur.pro/use-only-infrastructural-bundles-in-symfony/). I'll add a note about that in the documentation once having looked at what's better.
Let me look at it and come back with the alternatives I can think about.
Thanks for playing with the jwt user provider!
Hi @chalasr,
Firstly, thank you for the advice. I will decouple them.
However, I have found a kind of solution. Now, everything works fine, I have no extra query.
AppBundle\EntityUser:
class User implements UserInterface, JWTUserInterface, EquatableInterface
{
public function isEqualTo(UserInterface $user)
{
$rolesDiff = array_diff(
$this->getRoles(),
$user->getRoles()
);
return $this->getUsername() === $user->getUsername() && count($rolesDiff) == 0;
}
}
JWTAuthenticatedListener:
class JWTAuthenticatedListener
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function onJWTAuthenticated(JWTAuthenticatedEvent $event)
{
$token = $event->getToken();
$user = $token->getUser();
$this->entityManager->getUnitOfWork()->createEntity(User::class, [
'id' => $user->getId(),
]);
$user = $this->entityManager->merge($user);
$token->setUser($user);
}
}
Nevertheless I still look forward to your solution.
Thanks.
@rolandrajko Your solution looks good to me. Thanks for sharing. Closing this as resolved
Most helpful comment
Hi @chalasr,
Firstly, thank you for the advice. I will decouple them.
However, I have found a kind of solution. Now, everything works fine, I have no extra query.
AppBundle\EntityUser:
JWTAuthenticatedListener:
Nevertheless I still look forward to your solution.
Thanks.