How can I have optional services in the Symfony framework? - php
I would like to be able to get the current logged in user's credentials (email, password, etc) from the container. So, this is what I did:
security.token:
class: Symfony\Component\Security\Core\Authentication\Token\TokenInterface
factory: ["#security.token_storage", "getToken"]
private: true
security.current_user_credentials:
class: Symfony\Component\Security\Core\User\UserInterface
factory: ["#security.token", "getUser"]
security.current_user:
class: AppBundle\Entity\User
factory: ["#security.current_user_credentials", "getUser"]
When I do this and I'm logged in, it works fine. However, when I'm logged out, I get this in dev.log:
[2015-06-22 12:28:11] php.CRITICAL: Fatal Error: Call to a member function getUser() on string {"type":1,"file":"/var/www/html/phoenix/app/cache/dev/appDevDebugProjectContainer.php","line":3107,"level":-1,"stack":[{"function":"getSecurity_CurrentUserService","type":"->","class":"appDevDebugProjectContainer","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2140,"args":[]},{"function":"get","type":"->","class":"Symfony\\Component\\DependencyInjection\\Container","file":"/var/www/html/phoenix/app/cache/dev/appDevDebugProjectContainer.php","line":674,"args":[]},{"function":"getCommandHistoryCreatorService","type":"->","class":"appDevDebugProjectContainer","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2140,"args":[]},{"function":"get","type":"->","class":"Symfony\\Component\\DependencyInjection\\Container","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1929,"args":[]},{"function":"lazyLoad","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1894,"args":[]},{"function":"getListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":99,"args":[]},{"function":"getListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":158,"args":[]},{"function":"getNotCalledListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php","line":48,"args":[]},{"function":"lateCollect","type":"->","class":"Symfony\\Component\\HttpKernel\\DataCollector\\EventDataCollector","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Profiler/Profiler.php","line":115,"args":[]},{"function":"saveProfile","type":"->","class":"Symfony\\Component\\HttpKernel\\Profiler\\Profiler","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php","line":146,"args":[]},{"function":"onKernelTerminate","type":"->","class":"Symfony\\Component\\HttpKernel\\EventListener\\ProfilerListener","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php","line":61,"args":[]},{"function":"call_user_func:{/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php:61}","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php","line":61,"args":[]},{"function":"__invoke","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1824,"args":[]},{"function":"call_user_func:{/var/www/html/phoenix/app/cache/dev/classes.php:1824}","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1824,"args":[]},{"function":"doDispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1757,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1918,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":124,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":3067,"args":[]},{"function":"terminate","type":"->","class":"Symfony\\Component\\HttpKernel\\HttpKernel","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2409,"args":[]},{"function":"terminate","type":"->","class":"Symfony\\Component\\HttpKernel\\Kernel","file":"/var/www/html/phoenix/web/app_dev.php","line":20,"args":[]},{"function":"{main}","file":"/var/www/html/phoenix/web/app_dev.php","line":0,"args":[]}]} []
Is it possible to make the security.current_user_credentials and security.current_user optional? Is this error caused by these services?
Recently I ran into a similar issue and if you try to access a route that does not exist you might see the same error. I was working on a task where I needed to get hold of logged in user in my service and this is how I achieved it
My services.yml
services:
student_application_subscriber:
class: namespace\YourBundle\EventListener\StudentApplicationSubscriber
arguments:
- #doctrine.orm.entity_manager
- #security.token_storage
- #security.authorization_checker
- #twig
This is my service class StudentApplicationSubscriber
namespace yournamespace\YourBundleBundle\EventListener;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class StudentApplicationSubscriber implements EventSubscriberInterface
{
protected $em;
protected $twig;
protected $tokenStorage;
protected $authChecker;
function __construct(EntityManager $em, $tokenStorage, $authChecker, $twig)
{
$this->em = $em;
$this->twig = $twig;
$this->tokenStorage = $tokenStorage;
$this->authChecker = $authChecker;
}
public static function getSubscribedEvents()
{
return array(
'kernel.request' => 'onKernelRequest'
);
}
public function onKernelRequest()
{
if (!$token = $this->tokenStorage->getToken()) {
return;
}
$user = $token->getUser();
if (!is_object($user)) {
// there is no user - the user may not be logged in
return;
}
//get details of logged in user
$get_user_details = $this->tokenStorage->getToken()->getUser();
//make sure to pull information when user is logged in
if ($this->authChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
//get user id of logged in user
$userId = $get_user_details->getId();
//perform your logic here
}
}
}
What are you trying to achieve?
At first sight I would suggest would be either having a kernel listener that would check if there is a User and performe the required actions, or check that in your security.current_user_credentials.
I guess, if you just pass to your service and add below logic inside that function, then it work for both annon and authenticated users:
function dummyFunction($securityContext)) {
$email = $username = '';
if($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
$email = $securityContext->getToken()->getUser()->getEmail();
$username = $securityContext->getToken()->getUser()->getUsername();
}
..........................
}
Related
Symfony - Authentication with an API Token - Request token user is null
For the record, I'm using PHP 7.0.0, in a Vagrant Box, with PHPStorm. Oh, and Symfony 3. I'm following the API Key Authentication documentation. My goal is: To allow the user to provide a key as a GET apiKey parameter to authenticate for any route, except the developer profiler etc obviously To allow the developer to write $request->getUser() in a controller to get the currently logged in user My problem is that, although I believe I've followed the documentation to the letter, I'm still getting a null for $request->getUser() in the controller. Note: I've removed error checking to keep the code short ApiKeyAuthenticator.php The thing that processes the part of the request to grab the API key from it. It can be a header or anything, but I'm sticking with apiKey from GET. Differences from documentation, pretty much 0 apart from that I'm trying to keep the user authenticated in the session following this part of the docs. class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface { public function createToken(Request $request, $providerKey) { $apiKey = $request->query->get('apiKey'); return new PreAuthenticatedToken( 'anon.', $apiKey, $providerKey ); } public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) { $apiKey = $token->getCredentials(); $username = $userProvider->getUsernameForApiKey($apiKey); // The part where we try and keep the user in the session! $user = $token->getUser(); if ($user instanceof ApiKeyUser) { return new PreAuthenticatedToken( $user, $apiKey, $providerKey, $user->getRoles() ); } $user = $userProvider->loadUserByUsername($username); return new PreAuthenticatedToken( $user, $apiKey, $providerKey, $user->getRoles() ); } public function supportsToken(TokenInterface $token, $providerKey) { return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey; } } ApiKeyUserProvider.php The custom user provider to load a user object from wherever it can be loaded from - I'm sticking with the default DB implementation. Differences: only the fact that I have to inject the repository into the constructor to make calls to the DB, as the docs allude to but don't show, and also returning $user in refreshUser(). class ApiKeyUserProvider implements UserProviderInterface { protected $repo; // I'm injecting the Repo here (docs don't help with this) public function __construct(UserRepository $repo) { $this->repo = $repo; } public function getUsernameForApiKey($apiKey) { $data = $this->repo->findUsernameByApiKey($apiKey); $username = (!is_null($data)) ? $data->getUsername() : null; return $username; } public function loadUserByUsername($username) { return $this->repo->findOneBy(['username' => $username]); } public function refreshUser(UserInterface $user) { // docs state to return here if we don't want stateless return $user; } public function supportsClass($class) { return 'Symfony\Component\Security\Core\User\User' === $class; } } ApiKeyUser.php This is my custom user object. The only difference I have here is that it contains doctrine annotations (removed for your sanity) and a custom field for the token. Also, I removed \Serializable as it didn't seem to be doing anything and apparently Symfony only needs the $id value to recreate the user which it can do itself. class ApiKeyUser implements UserInterface { private $id; private $username; private $password; private $email; private $salt; private $apiKey; private $isActive; public function __construct($username, $password, $salt, $apiKey, $isActive = true) { $this->username = $username; $this->password = $password; $this->salt = $salt; $this->apiKey = $apiKey; $this->isActive = $isActive; } //-- SNIP getters --// } security.yml # Here is my custom user provider class from above providers: api_key_user_provider: id: api_key_user_provider firewalls: # Authentication disabled for dev (default settings) dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false # My new settings, with stateless set to false secured_area: pattern: ^/ stateless: false simple_preauth: authenticator: apikey_authenticator provider: api_key_user_provider services.yml Obviously I need to be able to inject the repository into the provider. api_key_user_repository: class: Doctrine\ORM\EntityRepository factory: ["#doctrine.orm.entity_manager", getRepository] arguments: [AppBundle\Security\ApiKeyUser] api_key_user_provider: class: AppBundle\Security\ApiKeyUserProvider factory_service: doctrine.orm.default_entity_manager factory_method: getRepository arguments: ["#api_key_user_repository"] apikey_authenticator: class: AppBundle\Security\ApiKeyAuthenticator public: false Debugging. It's interesting to note that, in ApiKeyAuthenticator.php, the call to $user = $token->getUser(); in authenticateToken() always shows an anon. user, so it's clearly not being stored in the session. Also note how at the bottom of the authenticator we do actually return a new PreAuthenticatedToken with a user found from the database: So it's clearly found me and is returning what it's supposed to here, but the user call in the controller returns null. What am I doing wrong? Is it a failure to serialise into the session because of my custom user or something? I tried setting all the user properties to public as somewhere in the documentation suggested but that made no difference.
So it turns out that calling $request->getUser() in the controller doesn't actually return the currently authenticated user as I would have expected it to. This would make the most sense for this object API imho. If you actually look at the code for Request::getUser(), it looks like this: /** * Returns the user. * * #return string|null */ public function getUser() { return $this->headers->get('PHP_AUTH_USER'); } That's for HTTP Basic Auth! In order to get the currently logged in user, you need to do this every single time: $this->get('security.token_storage')->getToken()->getUser(); This does, indeed, give me the currently logged in user. Hopefully the question above shows how to authenticate successfully by API token anyway. Alternatively, don't call $this->get() as it's a service locator. Decouple yourself from the controller and inject the token service instead to get the token and user from it.
To get the currently logged in User inside your Controller simply call: $this->getUser(); This will refer to a method in Symfony's ControllerTrait, which basically wraps the code provided in Jimbo's answer. protected function getUser() { if (!$this->container->has('security.token_storage')) { throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".'); } if (null === $token = $this->container->get('security.token_storage')->getToken()) { return; } if (!is_object($user = $token->getUser())) { // e.g. anonymous authentication return; } return $user; }
Symfony/Monolog: Custom processor with FOSUserBundle data
I'd like to add the userId and username, provided by FOSUserBundle, to my Logger (Monolog). I followed this tutorial to be able to log the IP, it's working. My code below breaks, because $this->tokenStorage->getToken() is NULL, even if I'm logged in. services.yml monolog.processor.user: class: AppBundle\Log\UserProcessor arguments: [ "#request_stack", "#security.token_storage" ] tags: - { name: monolog.processor } AppBundle/Log/UserProcessor.php use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use AppBundle\Entity\User; class UserProcessor { private $requestStack; private $tokenStorage; public function __construct(RequestStack $requestStack, TokenStorageInterface $tokenStorage) { $this->requestStack = $requestStack; $this->tokenStorage = $tokenStorage; } public function __invoke(array $record) { $username = ''; $userId = 0; $user = $this->tokenStorage->getToken()->getUser(); // !!! ERROR !!! // $this->tokenStorage->getToken() is NULL if ($user instanceof User) { $username = $user->getUsername(); $userId = $user->getId(); } $record['extra']['user_id'] = $userId; $record['extra']['username'] = $username; return $record; } } Any ideas? Thanks in advance!
The problem is that the processor is called to log something before security has been able to act and get the current user. Just check if token exists and if it doesn´t don't log user
How do you inject a Symfony repository into a custom user provider?
I have successfully injected repositories into services before, but can't get injection to work in a custom user provider. It seems like the __construct() method is never getting called, so the repository is never made available. The way you are supposed to make a user provider is to implement the interface UserProviderInterface, but the UserProvider class you create doesn't extend another class that would have a constructor. When I try to access the repository using $this->personRepository->findStatusByUsername($username), I get the error: Using $this when not in object context One reason I think the constructor is never getting called is that it has two assignments to $this, but I'm not getting an error message about them. I need the Person repository to be injected so that I can check whether or not the person trying to authenticate is already approved in the app (status in the Person table = approved.) My services.yml file has these settings: parameters: ginsberg_transportation.user.class: Ginsberg\TransportationBundle\Services\User user_provider.class: Ginsberg\TransportationBundle\Security\User\UserProvider services: ginsberg_user: class: "%ginsberg_transportation.user.class%" user_provider: class: "%user_provider.class%" arguments: ["#ginsberg_person.person_repository", "#logger"] ginsberg_person.person_repository: class: Doctrine\ORM\EntityRepository factory_service: doctrine.orm.default_entity_manager factory_method: getRepository arguments: - Ginsberg\TransportationBundle\Entity\Person My UserProvider.php class starts like this: namespace Ginsberg\TransportationBundle\Security\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Ginsberg\TransportationBundle\Entity\Person; use Doctrine\ORM\EntityRepository; class UserProvider implements UserProviderInterface { private $personRepository; private $logger; public function __construct(\Ginsberg\TransportationBundle\Entity\PersonRepository $personRepository, \Monolog\Logger $logger) { $this->personRepository = $personRepository; $this->logger = $logger; } public function loadUserByUsername($uniqname) { $password = "admin"; $salt = ""; $roles = array(); if (self::is_authenticated() && self::is_approved()) { if (self::is_superuser()) { $roles[] = 'ROLE_SUPER_ADMIN'; } elseif (self::is_admin()) { $roles[] = 'ROLE_ADMIN'; } elseif (self::is_eligible() && self::is_approved()) { $roles[] = 'ROLE_USER'; } return new User($uniqname, $password, $salt, $roles); } throw new UsernameNotFoundException( sprintf('Username "%s" does not exist.', $uniqname)); } ... public static function is_approved() { $uniqname = self::get_uniqname(); $status = $this->personRepository->findStatusByUsername($username); return($status == 'approved') ? TRUE : FALSE; } I also tried using property injection too, but that didn't work either. Should I just make my UserProvider class extend another appropriate class that would make use of a constructor? If so, any thoughts on what class would be fitting? Thanks for any suggestions.
The problem is that the method is_approved() is static. You cannot use $this in a static context. You need to declare your method non-static.
Dynamic roles in symfony
I have been trying to assign dynamic roles to users in my application. I tried to use an event listener to do that, but it just added the dynamic role for that one http request. This is the function in my custom event listener that adds the role. public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) { $user = $this->security->getToken()->getUser(); $role = new Role; $role->setId('ROLE_NEW'); $user->addRole($role); } But I am not sure if this is even possible to do with event listeners. I just need to find a nice way how to add the roles for the whole time the user is logged. I would appreciate any help and suggestions.
I haven't tested this yet, but reading the cookbook this could work. This example is a modified version of the example in the cookbook to accomodate for your requirements. class DynamicRoleRequestListener { public function __construct($session, $security) { $this->session = $session; $this->security = $security; } public function onKernelRequest(GetResponseEvent $event) { if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) { // don't do anything if it's not the master request return; } if ($this->session->has('_is_dynamic_role_auth') && $this->session->get('_is_dynamic_role_auth') === true) { $role = new Role("ROLE_NEW"); //I'm assuming this implements RoleInterface $this->security->getRoles()[] = $role; //You might have to add credentials, too. $this->security->getUser()->addRole($role); } // ... } private $session; private $security; } And then you declare it as a service. services: kernel.listener.dynamicrolerequest: class: Your\DemoBundle\EventListener\DynamicRoleRequestListener arguments: [#session, #security.context] tags: - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } A similar question is here: how to add user roles dynamically upon login with symfony2 (and fosUserBundle)?
Anonymous user object in symfony
I'm using the basic user login/logout system provided with Symfony and it works fine as long as people log in. In that case the $user object is always provided as needed. The problem is then when logged out (or not lgged in yet) there is no user object. Is there a possibility to have (in that case) a default user object provided with my own default values? Thanks for your suggestions
Because the solution mention above by #Chopchop (thanks anyway for your effort) didn't work here I wrote a little workaround. I created a new class called myController which extends Controller. The only function i override is the getUser() function. There I implement it like this: public function getUser() { $user = Controller::getUser(); if ( !is_object($user) ) { $user = new \ACME\myBundle\Entity\User(); $user->setUserLASTNAME ('RaRa'); $user->setID (0); // etc... } return $user; } This works fine for me now. The only problem is that you really have to be careful NOT to forget to replace Controller by myController in all your *Controller.php files. So, better suggestions still welcome.
Works in Symfony 3.3 Using the suggestion of #Sfblaauw, I came up with a solution that uses a CompilerPass. AppBundle/AppBundle.php class AppBundle extends Bundle { public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new OverrideAnonymousUserCompilerPass()); } } OverrideAnonymousUserCompilerPass.php class OverrideAnonymousCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $definition = $container->getDefinition('security.authentication.listener.anonymous'); $definition->setClass(AnonymousAuthenticationListener::class); } } AnonymousAuthenticationListener.php class AnonymousAuthenticationListener implements ListenerInterface { private $tokenStorage; private $secret; private $authenticationManager; private $logger; public function __construct(TokenStorageInterface $tokenStorage, $secret, LoggerInterface $logger = null, AuthenticationManagerInterface $authenticationManager = null) { $this->tokenStorage = $tokenStorage; $this->secret = $secret; $this->authenticationManager = $authenticationManager; $this->logger = $logger; } public function handle(GetResponseEvent $event) { if (null !== $this->tokenStorage->getToken()) { return; } try { // This is the important line: $token = new AnonymousToken($this->secret, new AnonymousUser(), array()); if (null !== $this->authenticationManager) { $token = $this->authenticationManager->authenticate($token); } $this->tokenStorage->setToken($token); if (null !== $this->logger) { $this->logger->info('Populated the TokenStorage with an anonymous Token.'); } } catch (AuthenticationException $failed) { if (null !== $this->logger) { $this->logger->info('Anonymous authentication failed.', array('exception' => $failed)); } } } } This file is a copy of the AnonymousAuthenticationListener that comes with Symfony, but with the AnonymousToken constructor changed to pass in an AnonymousUser class instead of a string. In my case, AnonymousUser is a class that extends my User object, but you can implement it however you like. These changes mean that {{ app.user }} in Twig and UserInterface injections in PHP will always return a User: you can use isinstance to tell if it's an AnonymousUser, or add a method isLoggedIn to your User class which returns true in User but false in AnonymousUser.
you can redirect the user not authenticated and force a fake login (to create a user ANONYMOUS) and set it as well on logout public function logoutAction(){ $em = $this->getDoctrine()->getManager(); $user = $em->getRepository('VendorBundle:User')->findByUserName('annonymous'); $session = $this->getRequest()->getSession(); $session->set('user', $user); } and if user is not set public function checkLoginAction(){ if(!$session->get('user')){ $em = $this->getDoctrine()->getManager(); $user = $em->getRepository('VendorBundle:User')->findByUserName('annonymous'); $session = $this->getRequest()->getSession(); $session->set('user', $user); } //this->redirect('/'); } in you security.yml security: firewalls: main: access_denied_url: /check_login/ access_control: - { path: ^/$, role: ROLE_USER } This is only an example i haven't tested (and will probably don't, since i don't get the purpose of doing this:) )
Using Symfony 2.6 Like Gordon says use the authentication listener to override the default anonymous user. Now you can add the properties that you need to the anonymous user, in my case the language and the currency. security.yml parameters: security.authentication.listener.anonymous.class: AppBundle\Security\Http\Firewall\AnonymousAuthenticationListener AnonymousAuthenticationListener.php namespace AppBundle\Security\Http\Firewall; ... use AppBundle\Security\User\AnonymousUser; class AnonymousAuthenticationListener implements ListenerInterface { ... public function handle(GetResponseEvent $event) { ... try { $token = new AnonymousToken($this->key, new AnonymousUser(), array()); ... } } } AnonymousUser.php class AnonymousUser implements UserInterface { public function getUsername() { return 'anon.'; } }