Why does TokenStorage behave incorrectly between Behat scenarios? - symfony

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?

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 twig service user from token storage is null sometimes

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.

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

Symfony 2.8 Twig extension based on current logged user

What I want to do is show logged user only content that he has access to.
First thing I was doing access_control in security.yml & redirect but i must do at least 30 deferent accounts;/
Next i create twig extension that will connect to DB and get current logged user specific settings -
access to panels. Is this good way?
The problem is
$user = $this->getUser()->getId();
$currentUser = $this->em->getRepository('AppBundle:User')->find($user);
It will not work, Blank page appears in dev env
But when i put 1
$currentUser = $this->em->getRepository('AppBundle:User')->find(1);
& 1 is user id everything is ok.
services.yml
app.twig.users_extension:
class: AppBundle\Twig\Extension\AccesExtension
arguments: ["#doctrine.orm.entity_manager","#security.token_storage"]
tags:
- { name: twig.extension }
Twig Extension
class AccesExtension extends \Twig_Extension
{
protected $em;
protected $tokenStorage;
public function __construct(EntityManager $em, TokenStorage $tokenStorage)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
public function getUser()
{
return $this->tokenStorage->getToken()->getUser();
}
public function getGlobals()
{
$user = $this->getUser()->getId();
$currentUser = $this->em->getRepository('AppBundle:User')->find($user);
return array (
"acces" => $currentUser,
);
}
public function getName()
{
return "AppBundle:AccesExtension ";
}
}
As people in the comments already told you, you can use security voters.
Here is a simple tutorial with video/text that explains the use of voters and also how to use them in twig, this should help you with the problem you have.

Dynamically adding roles to a user

We are using Symfony2's roles feature to restrict users' access to certain parts of our app. Users can purchase yearly subscriptions and each of our User entities has many Subscription entities that have a start date and an end.
Now, is there a way to dynamically add a role to a user based on whether they have an 'active' subscription? In rails i would simply let the model handle whether it has the necessary rights but I know that by design symfony2 entities are not supposed to have access to Doctrine.
I know that you can access an entity's associations from within an entity instance but that would go through all the user's subscription objects and that seems unnecessaryly cumbersome to me.
I think you would do better setting up a custom voter and attribute.
/**
* #Route("/whatever/")
* #Template
* #Secure("SUBSCRIPTION_X")
*/
public function viewAction()
{
// etc...
}
The SUBSCRIPTION_X role (aka attribute) would need to be handled by a custom voter class.
class SubscriptionVoter implements VoterInterface
{
private $em;
public function __construct($em)
{
$this->em = $em;
}
public function supportsAttribute($attribute)
{
return 0 === strpos($attribute, 'SUBSCRIPTION_');
}
public function supportsClass($class)
{
return true;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
// run your query and return either...
// * VoterInterface::ACCESS_GRANTED
// * VoterInterface::ACCESS_ABSTAIN
// * VoterInterface::ACCESS_DENIED
}
}
You would need to configure and tag your voter:
services:
subscription_voter:
class: SubscriptionVoter
public: false
arguments: [ #doctrine.orm.entity_manager ]
tags:
- { name: security.voter }
Assuming that you have the right relation "subscriptions" in your User Entity.
You can maybe try something like :
public function getRoles()
{
$todayDate = new DateTime();
$activesSubscriptions = $this->subscriptions->filter(function($entity) use ($todayDate) {
return (($todayDate >= $entity->dateBegin()) && ($todayDate < $entity->dateEnd()));
});
if (!isEmpty($activesSubscriptions)) {
return array('ROLE_OK');
}
return array('ROLE_KO');
}
Changing role can be done with :
$sc = $this->get('security.context')
$user = $sc->getToken()->getUser();
$user->setRole('ROLE_NEW');
// Assuming that "main" is your firewall name :
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null, 'main', $user->getRoles());
$sc->setToken($token);
But after a page change, the refreshUser function of the provider is called and sometimes, as this is the case with EntityUserProvider, the role is overwrite by a query.
You need a custom provider to avoid this.

Resources