Symfony twig service user from token storage is null sometimes - symfony

I have a service that I have defined as a global twig variable that makes use of autowiring the TokenStorageInterface in order to get the current logged in user.
Sometimes the token is null and throws an exception when trying to access a null object. Call to a member function getUser() on null
This is the barebone code that breaks.
BonusService.php
namespace AppBundle\Service;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class BonusService {
private $user;
private $manager;
__construct(TokenStorageInterface, $tokenStorage, ObjectManager $manager) {
$this->user = $tokenStorage->getToken()->getUser(); // Sometimes fails here
$this->manager = $manager;
}
public function hasBonuses() {
return count($this->manager->getRepository(Bonus::class)->findBy(array('contact' => $user)) > 0;
}
}
services.yml
services:
_defaults:
autowire: true
autoconfigure: true
public: true
AppBundle\Service\BonusService:
config.yml
twig:
...
globals:
bonus_service: '#AppBundle\Service\BonusService'
index.html.twig
...
{% if bonus_service.hasBonuses %}Have Bonuses{% endif %}
...
I've been googling reasons as to why the token storage may be null when twig is doing it's thing. One issue predominately came appeared is to assure that my route is behind a firewall, which in this case it is and requires an authenticated user.
Also with noting is that I have a similar service with an identical constructor which is utilised within a controller. When the BonusService doesn't decide to use a null token and the page loads, the other service has no problem grabbing a token. When I remove the call to the service in twig, the page loads 100% of the time, even with the other service and it's identical constructor.
Any help would be much appreciated!

When creating services your constructor should avoid executing much beyond storing the injected services as a reference.
class BonusService {
private $tokenStorage;
private $manager;
public function __construct(TokenStorageInterface $tokenStorage, ObjectManager $manager) {
$this->tokenStorage = $tokenStorage;
$this->manager = $manager;
}
public function hasBonuses() {
if (!$this-tokenStorage->getToken() instanceof User) {
return false;
}
return count($this->manager->getRepository(Bonus::class)->findBy(array(
'contact' => $this-tokenStorage->getToken()->getUser())
) > 0;
}
}
You still have to check if the token is set and is an instance of User (or however your User is called).
The reason you should not use any of the injected services at in the constructor is because at that stage the container is still booting up and building all the services. So your dependencies might not yet be initialized fully.

Related

Retrieve current user in Symfony app while respecting LoD

I'm having some issues understanding how the Law of Demeter should be applied in some cases with Symfony's DI system.
I have some factory that requires to access current logged in user in the application. To do that I need to require #security.token_storage to inject it as a constructor argument.
But in my factory, to access the user I will need to do : $tokenStorage->getToken()->getUser(), and worst, if I want to access some property of my user, I will need to dive one level deeper.
How would you fix this issue according to the law of demeter ?
Here is a sample of my code :
class SomeFactory
{
/**
* #var User
*/
private $currentUser;
/**
* #param TokenStorageInterface $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->currentUser = $this->setCurrentUser($tokenStorage);
}
/**
* #param TokenStorageInterface $tokenStorage
*/
protected function setCurrentUser(TokenStorageInterface $tokenStorage)
{
if ($tokenStorage->getToken()
&& $tokenStorage->getToken()->getUser()
&& in_array('ADMIN_ROLE', $tokenStorage->getToken()->getUser()->getRoles())
) {
$this->currentUser = $tokenStorage->getToken()->getUser();
}
}
}
I hope i am being clear.
Thank you very much :)
It seems that in DI factories the session has not been initialized, which makes the current user token unavailable, at this point.
What I did to make it work:
Register a new event listener for kernel.event:
services:
before_request_listener:
class: App\EventListener\MyRequestListener
tags:
-
name: kernel.event_listener
event: kernel.request
method: onKernelRequest
In MyRequestListener I've added my service as dependency (which invokes the factory) and provides the service, with incomplete data. Also I require Security:
public function __construct(\Symfony\Component\Security\Core\Security $security, MyService $service)
{
$this->security = $security;
$this->service = $service;
}
Now, in MyRequestListener::onKernelRequest I can access the user's data and add/change the incomplete properties of my service.
public function onKernelRequest(): void
{
if ($user = $this->security->getUser()) {
$this->service->whatever = $user->whatever;
}
}
Because Symfony uses the same instance of MyService, those modification will be available in all further services.
But keep in mind, that your service, also needs to deal with the incomplete data, when no active user session is existing (e.g. because no user is logged in, or on CLI).

Symfony 4 Doctrine EventSubscriber not used

Trying to register a Doctrine EventSubscriber but nothing is ever actually fired.
I have, on the Entity, in question, set the #ORM\HasLifeCycleCallbacks annotation.
Here's the Subscriber:
<?php
namespace App\Subscriber;
use App\Entity\User;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserPasswordChangedSubscriber implements EventSubscriber
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function getSubscribedEvents()
{
return [Events::prePersist, Events::preUpdate, Events::postLoad];
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
private function updateUserPassword(User $user)
{
$plainPassword = $user->getPlainPassword();
if (!empty($plainPassword)) {
$encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword);
$user->setPassword($encodedPassword);
$user->eraseCredentials();
}
}
}
The part that is making this particuarly frustrating is that this same code and configuration was fine in Symfony 3 whe autowiring was turned off and I manually coded all my services.
However, now, even if I manually code up a service entry for this, in the usual way, still nothing happens.
EDIT:
Here is my services.yaml after trying what suggested Domagoj from the Symfony docs:
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
It didn't work. Interestingly, If I un-implement the EventSubscriber interface, Symfony throws an exception (rightly). Yet my break points in the code are completely ignored.
I've considered an EntityListener, but it cannot have a constructor with arguments, doesn't have access to the Container and I shouldn't have to; this ought to work :/
I ended up figuring this out. The field that I was specifically updating was transient, and therefore Doctrine didn't consider this an Entity change (rightly).
To fix this, I put
// Set the updatedAt time to trigger the PreUpdate event
$this->updatedAt = new DateTimeImmutable();
In the Entity field's set method and this forced an update.
I also did need to manually register the Subscriber in the services.yaml using the following code. symfony 4 autowiring wasn't auto enough for a Doctrine Event Subscriber.
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
For your first problem, doctrine event subscribers are not autoconfigured/auto-tagged. For the reasons and solutions, you have some responses here.
Personnaly, I just have one Doctrine ORM mapper, so I put this in my services.yaml file :
services:
_instanceof:
Doctrine\Common\EventSubscriber:
tags: ['doctrine.event_subscriber']
You have to register your Event Listener as a service and tag it as doctrine.event_listener
https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html#configuring-the-listener-subscriber

Subscriber call twice symfony

I use FOSUserEvents after submit form but the subscriber call twice.
In this way my captcha is valid the first time and not valid the second
this is my code
<?php
namespace AppBundle\EventListener;
class CaptchaSubscriber implements EventSubscriberInterface
{
private $router;
private $requestStack;
private $templating;
/**
* RedirectAfterRegistrationSubscriber constructor.
*/
public function __construct(RouterInterface $router, RequestStack $requestStack, \Twig_Environment $templating)
{
$this->router = $router;
$this->requestStack = $requestStack;
$this->templating = $templating;
}
public function onRegistrationInit(GetResponseUserEvent $event)
{
if ($this->requestStack->getMasterRequest()->isMethod('post')) {
...handle captcha...
}
}
public static function getSubscribedEvents()
{
return [
FOSUserEvents::REGISTRATION_INITIALIZE => 'onRegistrationInit'
];
}
}
my symfony is 3.3
UPDATE
I added
$event->stopPropagation();
with this snippet the code works, but i don't know if it is the best practice
In my case of symfony 4.2 it depends on the service definition if it occures or not.
My Subscriber gets registered twice if I define the service like this:
# oauth process listener
app.subscriber.oauth:
class: App\EventListenerSubscriber\OauthSubscriber
arguments: ['#session', '#router', '#security.token_storage', '#event_dispatcher', '#app.entity_manager.user', '#app.fos_user.mailer.twig_swift']
tags:
- { name: kernel.event_subscriber }
But it gets registerd only once if I chenge the definition to this:
# oauth process listener
App\EventListenerSubscriber\OauthSubscriber:
arguments: ['#session', '#router', '#security.token_storage', '#event_dispatcher', '#app.entity_manager.user', '#app.fos_user.mailer.twig_swift']
tags:
- { name: kernel.event_subscriber }
I posted a bug report on github and got immediately an answer, that in newer symfony versions event listeners and subscribers get registered automatically with their class name as key (under some default conditions - must read on that topic).
So there is no need to register them explicitely as services.
I we do this anyway, but using an arbitrary key instead of class name, there will be two services.
If you are using Autowiring/Autoconfiguration, it's possible that you've added the subscriber service you show above, twice. I've done it myself when I first added the autowiring, but I also had the subscriber listed explicitly in the configuration as well.
You can see what events are registered (and check if any are registered more than once to perform the same service/action) with:
bin/console debug:event-dispatcher

Why does TokenStorage behave incorrectly between Behat scenarios?

In our Symfony applications we have custom code that handles authentication. It's based on top of thephpleague/oauth2-server.
We have 2 types of authentication, both of which end up calling TokenStorage::setToken().
All this works perfectly.
Now we decided to add some Behat tests to our bundle:
Scenario: I am able to login with type 1
When I login with type 1
Then I am successfully logged in as a "type1" member
Scenario: I am able to login with type 2
When I login with type 2
Then I am successfully logged in as a "type2" member
(I changed the actual wording of the scenarios in order not to expose business-specific terms)
In order to test the Then steps, I need access to the TokenStorage in my Contexts because I basically want to test if the user has the correct security role:
default:
gherkin:
cache: null
suites:
mybundle:
paths: [ %paths.base%/src/My/Bundle/Features ]
contexts:
- My\Bundle\Context\LoginContext
tokenStorage: '#security.token_storage'
extensions:
Behat\Symfony2Extension:
kernel:
env: "test"
debug: "true"
My context looks like this:
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Behat\Behat\Context\Context;
class LoginContext implements Context
{
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #Then I am successfully logged in as a :expectedRole member
*/
public function iAmSuccessfullyLoggedInAsAMember($expectedRole)
{
$found = false;
foreach ($this->tokenStorage->getToken()->getRoles() as $role) {
if ($role->getRole() == $expectedRole) {
$found = true;
break;
}
}
if (! $found) {
throw new \Exception('Incorrect role');
}
}
}
What happens is:
if I run only the first scenario, it works OK
if I run only the second scenario, it works OK
if I run both of them, then on the 2nd scenario I get the error Call to a member function getRoles() on a non-object
Why does this happen ? How can I make it work correctly ?
My versions are:
behat/behat: 3.1.0
behat/gherkin: 4.4.1
behat/symfony2-extension: 2.1.1
symfony/symfony: 2.6.13
One solution I tried is to have my context implement the Behat\Symfony2Extension\Context\KernelAwareContext interface instead and then I did this:
use Behat\Symfony2Extension\Context\KernelAwareContext;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class LoginContext implements KernelAwareContext
{
private $tokenStorage;
private $kernel;
public function setKernel(KernelInterface $kernel)
{
$this->kernel = $kernel;
}
/**
* #BeforeScenario
*/
public function getDependencies(BeforeScenarioScope $scope)
{
$container = $this->kernel->getContainer();
$this->tokenStorage = $container->get('security.token_storage');
}
}
My thinking was that, by explicitly retrieving TokenStorage before each scenario, I would get a fresh instance each time and therefore it would work.
However, it behaved exactly the same :( .
What am I missing?

How to impersonate user by id instead of username in symfony?

I can't figure out how to impersonate a user by user's id instead of user's username in Symfony?
The following trick which works with username can't work with id, as symfony is looking for username:
?_switch_user={id}
This is impossible to do without implementing your own firewall listener, as behind the scenes it loads the user from the userprovider (which only has a loadUserByUsername() method in its interface).
You could however implement your own firewall listener and get inspired by having a look at the code in Symfony\Component\Security\Http\Firewall\SwitchUserListener. For detailed information on implementing your own authentication provider, check the cookbook article.
EDIT:
One possible solution might be registering an extra request listener:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LookupSwitchUserListener implements EventSubscriberInterface
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['lookup', 12] // before the firewall
];
}
public function lookup(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->has('_switch_user') {
return; // do nothing if already a _switch_user param present
}
if (!$id = $request->query->has('_switch_user_by_id')) {
return; // do nothing if no _switch_user_by_id param
}
// lookup $username by $id using the repository here
$request->attributes->set('_switch_user', $username);
}
}
Now register this listener in the service container:
services:
my_listener:
class: LookupSwitchUserListener
tags:
- { name: kernel.event_subscriber }
Calling a url with the ?_switch_user_by_id=xxx parameter should now correctly look up the username and set it so the SwitchUserListener can switch to the specified user.

Resources