In the past two days I've been trying to make my custom LocaleSubscriber, a subscriber which is supposed to set user's locale to user's preference or to default.
<?php
namespace App\Event\Subscriber;
use App\Entity\Language;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
/**
* Class LocaleSubscriber
* #package App\Event\Subscriber
*/
class LocaleSubscriber
{
/** #var EntityManagerInterface */
private $entityManager;
/** #var SessionInterface */
private $session;
/**
* LocaleSubscriber constructor.
*
* #param EntityManagerInterface $entityManager
* #param SessionInterface $session
*/
public function __construct(EntityManagerInterface $entityManager, SessionInterface $session)
{
$this->entityManager = $entityManager;
$this->session = $session;
}
/** {#inheritdoc} */
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
$user = $event->getAuthenticationToken()->getUser();
if (null !== $user->getLocale()) {
$this->session->set('_locale', $user->getLocale());
$request->setLocale($user->getLocale());
} else {
/** #var Language $language */
$language = $this->entityManager->getRepository(Language::class)->findOneBy(['defaultLang' => true]);
$language = $language->getSlug();
$this->session->set('_locale', $language);
$request->setLocale($language);
}
}
}
I'm trying to do this system because all languages are listed in my database (Language is one of my entities) and I want to make this application easy to maintain and extend for non-technical users.
The problem is that after leaving this method, request's locale is back to en and the new locale is kept only in session.
I've dumped at the end of the function dump($request, $this->session->all()) and both have the same language. Also, the priority in services.yaml is set to 15.
App\Event\Subscriber\LocaleSubscriber:
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin, priority: 15 }
Any idea why is not working? Did someone encountered the same problem? Any examples?
Related
I want to recover the date of the last time a user logged in my Symfony 5 website, I created a LoginListener and did the right settings (So-think-I ?) to make it work but in the class Login Listener :
namespace App\Event;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class LoginListener
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
// Get the User entity.
$user = $event->getAuthenticationToken()->getUser();
// Update your field here.
$user->setLastLogin(new \DateTime());
// Persist the data to database.
$this->em->persist($user);
$this->em->flush();
}
}
The setLastLogin(new \DateTime()) is described as an undefined method. Yet this method is in the entity User.php and it is properly called in the loginlistener. And the "use App\Entity\User :
/**
* {#inheritdoc}
*/
public function setLastLogin(\DateTime $time = null)
{
$this->lastLogin = $time;
return $this;
}
/**
* Gets the last login time.
*
* #return \DateTime|null
*/
public function getLastLogin()
{
return $this->lastLogin;
}
/**
* #var \DateTime $lastLogin
*
* #ORM\Column(type="datetime")
*/
private $lastLogin;
And when I try to make a exit(var_dump($user), nothing appears. Here is my services.yaml :
App\EventListener\LoginListener:
- tags:
- { name: 'kernel.event_listener', event: 'security.interactive_login', entity: 'App\Entity\User' }
Can someone help me please ? Thank you.
I've just upgraded an app from Symfony 4.3.9 to 4.4.2. After that, I had the debug bar not working and showing "An error occurred while loading the web debug toolbar. "
After a long investigation, I found that it's because of an EventListener on security.authentication.failure event that was the cause.
Commenting the onAuthenticationFailure method content did nothing and after some investigation it works when removing the TokenStorageInterface from the tags and constructor... But I need it.
Any ideas?
Here's the code :
services.yaml
App\EventListener\LoginListener:
arguments: ["#doctrine", "#security.token_storage", "#router", "#event_dispatcher"]
tags:
- { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
LoginListener.php
<?php
namespace App\EventListener;
use App\Entity\AdminUser;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
/**
* Class LoginListener
* Listens to user log in events (failure, interactive log in) to provide additionnal security measures
*
* #package App\EventListener
*/
final class LoginListener
{
protected $doctrine;
protected $request;
protected $tokenStorage;
protected $router;
protected $dispatcher;
/**
* Login constructor.
*
* #param Registry $doctrine
* #param TokenStorageInterface $tokenStorage
* #param RouterInterface $router
* #param EventDispatcherInterface $dispatcher
*/
public function __construct(
Registry $doctrine,
TokenStorageInterface $tokenStorage,
RouterInterface $router,
EventDispatcherInterface $dispatcher
) {
$this->doctrine = $doctrine;
$this->tokenStorage = $tokenStorage;
$this->router = $router;
$this->dispatcher = $dispatcher;
}
/**
* #param AuthenticationFailureEvent $event
* #throws ORMException
* #throws OptimisticLockException
*/
public function onAuthenticationFailure(AuthenticationFailureEvent $event)
{
/** #var EntityManager $em */
$em = $this->doctrine->getManager();
$username = $event->getAuthenticationToken()->getUsername();
/** #var AdminUser $user */
$user = $em->getRepository(AdminUser::class)->findOneBy(['username' => $username]);
if ($user instanceof AdminUser) {
$user->addFailedLogin();
if ($user->getFailedLogin() == 5) {
$user->setLocked(1);
}
$em->persist($user);
$em->flush();
}
}
}
Thanks :)
---EDIT---
In fact that listener was an edit from another one. It doesn't need the TokenStorage but I'll have the problem in that one in a near future then :
<?php
namespace App\XXXBundle\EventListener;
use App\XXXBundle\Entity\AdminUser;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
/**
* Class Login
* Listens to user log in events (failure, interactive log in) to provide additionnal security measures
*
* #package App\XXXBundle\EventListener
*/
class Login
{
protected $doctrine;
protected $request;
protected $tokenStorage;
protected $router;
protected $dispatcher;
/**
* Login constructor.
*
* #param Registry $doctrine
* #param TokenStorage $tokenStorage
* #param RouterInterface $router
* #param EventDispatcherInterface $dispatcher
*/
public function __construct(
Registry $doctrine,
TokenStorage $tokenStorage,
RouterInterface $router,
EventDispatcherInterface $dispatcher
) {
$this->doctrine = $doctrine;
$this->tokenStorage = $tokenStorage;
$this->router = $router;
$this->dispatcher = $dispatcher;
}
/**
* #param AuthenticationFailureEvent $event
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
public function onAuthenticationFailure(AuthenticationFailureEvent $event)
{
/** #var EntityManager $em */
$em = $this->doctrine->getManager();
$userName = $event->getAuthenticationToken()->getUsername();
/** #var AdminUser $user */
$user = $em->getRepository(AdminUser::class)->findOneByUsername($userName);
if ($user instanceof AdvancedUserInterface) {
$user->addFailedLogin();
if ($user->getFailedLogin() == 5) {
$user->setLocked(1);
}
$em->persist($user);
$em->flush();
}
}
/**
* #param InteractiveLoginEvent $event
* #throws \Exception
*/
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if ($user instanceof AdvancedUserInterface) {
$em = $this->doctrine->getManager();
if ($user->getLocked()) {
$this->tokenStorage->setToken(null);
throw new CustomUserMessageAuthenticationException('Compte verrouillé.');
}
if ($user->getExpiresAt() && $user->getExpiresAt() <= new \DateTime()) {
$user->setIsActive(0);
$em->persist($user);
$em->flush();
$this->tokenStorage->setToken(null);
throw new CustomUserMessageAuthenticationException('Compte expiré.');
}
if ($user->getCredentialsExpireAt() && $user->getCredentialsExpireAt() <= new \DateTime()) {
$this->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'redirectToCredentialsChange']);
}
$user->setLastLogin(new \DateTime());
$user->setFailedLogin(0);
$em->persist($user);
$em->flush();
}
}
public function redirectToCredentialsChange(ResponseEvent $event)
{
$event->getResponse()->headers->set('Location', $this->router->generate('admin_security_changecredentials'));
}
}
I'm trying to run a simple auth test using PHPUnit with Symfony 4 to see if the user has successfully logged in. I followed the documentation for running a auth test. The /admin page will automatically redirect to /login if the user is not signed in.
I'm getting the this error on running the test:
There was 1 failure:
1) App\Tests\AuthTest::testAdminPanel
Failed asserting that 302 is identical to 200.
~config/packages/test/security.yaml:
security:
firewalls:
main:
http_basic: ~
Test:
public function testAdminPanel() {
$client = static::createClient(array(), array(
'PHP_AUTH_USER' => 'username',
'PHP_AUTH_PW' => 'password',
));
$crawler = $client->request('GET', '/admin');
$this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
}
Note: The username account along with the password, password, do exist in the database.
Security Controller:
<?php
namespace App\Security;
use App\Form\LoginFormType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
/**
* Class LoginFormAuthenticator
* #package App\Security
*/
class LoginFormAuthenticator extends AbstractGuardAuthenticator
{
/**
* #var FormFactoryInterface
*/
private $formFactory;
/**
* #var EntityManager
*/
private $em;
/**
* #var RouterInterface
*/
private $router;
/**
* #var UserPasswordEncoder
*/
private $userPasswordEncoder;
/**
* LoginFormAuthenticator constructor.
* #param FormFactoryInterface $formFactory
* #param EntityManagerInterface $em
* #param RouterInterface $router
* #param UserPasswordEncoderInterface $userPasswordEncoder
*/
public function __construct(FormFactoryInterface $formFactory, EntityManagerInterface $em, RouterInterface $router, UserPasswordEncoderInterface $userPasswordEncoder)
{
$this->formFactory = $formFactory;
$this->em = $em;
$this->router = $router;
$this->userPasswordEncoder = $userPasswordEncoder;
}
/**
* #return string
*/
protected function getLoginUrl()
{
return $this->router->generate('login');
}
/**
* #param Request $request
* #return bool
*
* Called on every request to decide if this authenticator should be
* used for the request. Returning false will cause this authenticator
* to be skipped. Current implementation checks that this request method is POST and
* that the user is on the login page.
*
*/
public function supports(Request $request)
{
return ($request->attributes->get('_route') === 'login' && $request->isMethod('POST'));
}
/**
* #param Request $request
* #param AuthenticationException|null $authException
* #return RedirectResponse|Response
*/
public function start(Request $request, AuthenticationException $authException = null)
{
return new RedirectResponse($this->router->generate('login'));
}
/**
* #param Request $request
* #return bool|mixed
*/
public function getCredentials(Request $request)
{
$form = $this->formFactory->create(LoginFormType::class);
$form->handleRequest($request);
$data = $form->getData();
$request->getSession()->set(
Security::LAST_USERNAME,
$data['username']
);
return $data;
}
/**
* #param mixed $credentials
* #param UserProviderInterface $userProvider
* #return null|object|UserInterface
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
$user = $this->em->getRepository('App:User')->findOneBy(['username' => $username]);
if(empty($user)) {
$user = $this->em->getRepository('App:User')->findOneBy(array('email' => $username));
}
return $user;
}
/**
* #param mixed $credentials
* #param UserInterface $user
* #return bool
*/
public function checkCredentials($credentials, UserInterface $user)
{
$password = $credentials['password'];
if($this->userPasswordEncoder->isPasswordValid($user, $password)) {
return true;
}
return false;
}
/**
* #return string
*/
public function getDefaultSuccessRedirectUrl()
{
return $this->router->generate('default');
}
/**
* #param Request $request
* #param AuthenticationException $exception
* #return RedirectResponse
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse($this->router->generate('login'));
}
/**
* #param Request $request
* #param TokenInterface $token
* #param string $providerKey
* #return null|RedirectResponse|Response
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$user = $token->getUser();
$user->setLastLogin(new \DateTime('now'));
$this->em->persist($user);
$this->em->flush();
return new RedirectResponse($this->router->generate('easyadmin'));
}
/**
* #return bool
*/
public function supportsRememberMe()
{
return false;
}
}
You are using AbstractGuardAuthenticator class to authenticate your users. Following the link you provided, you have to use PostAuthenticationGuardToken class.
public function testAdminPanel(): void
{
// Create client
$client = static::createClient();
// Get user
$user = ...; // The user entity
// Login the user
$this->simulateLogin($user);
// Perform request
$crawler = $client->request('GET', '/admin');
// Assert
$this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
}
private function simulateLogin(User $user): void
{
// Get session
$session = self::$container->get('session');
// Set firewall
$firewall = '...'; // Your firewall name
// Authenticate the user
$token = new PostAuthenticationGuardToken($user, $firewall, $user->getRoles());
$session->set('_security_' . $firewall, serialize($token));
$session->save();
// Set cookie
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
To be able to use this method, you can create a fixture to create the user in your test database. See Doctrine Fixture Bundle and even use Liip Test Fixture Bundle to be able to load the fixture and truncate the database at each functional test.
I am using Symfony 5, but I guess it will not be very different.
Hope it will help !
your test seems fine. HTTP 302 means redirect, we are usually redirected after a successfull login.
try to add $client->followRedirects();
as suggested at https://symfony.com/doc/current/testing#redirecting
I am using symfony 2.8 and just did a composer udate on my local machine and started getting this kind of errors.
Full error (happens for both for mailer and user manager, that were working smoothly for a year...)
[Symfony\Component\DependencyInjection\Exception\InvalidArgumentException]
Unable to replace alias "fos_user.user_manager" with actual definition "my.custom_user_manager".
[Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException]
You have requested a non-existent service "my.custom_user_manager".
config.yml
fos_user:
service:
user_manager: my.custom_user_manager
mailer: my.custom_user_mailer # fos_user.mailer.twig
It happens for a custom user manager of have overriden as well. My FOS user bundle version is v2.0.0-alpha3 and has not changed since precedent version.
My service definitions used to / and look correct.
Any lead here ?
Service definitions:
// bunch of "use" here...
/**
* #DI\Service("my.custom_user_manager", public=true)
*/
class CoreUserManager extends BaseUserManager
{
/**
* #DI\InjectParams({
* "encoderFactory" = #DI\Inject("security.encoder_factory"),
* "usernameCanonicalizer" = #DI\Inject("fos_user.util.username_canonicalizer"),
* "emailCanonicalizer" = #DI\Inject("fos_user.util.email_canonicalizer"),
* "em" = #DI\Inject("doctrine.orm.entity_manager"),
* })
*/
public function __construct(
EncoderFactoryInterface $encoderFactory,
CanonicalizerInterface $usernameCanonicalizer,
CanonicalizerInterface $emailCanonicalizer,
EntityManager $em
)
{
$class = 'Medical\CoreBundle\Entity\User';
parent::__construct($encoderFactory, $usernameCanonicalizer, $emailCanonicalizer, $em, $class);
}
And the mailer
// bunch of proper "use"... here
/**
* Extends FOS mailer to send your own emails and add methods
* #DI\Service("my.custom_user_mailer", public=false)
*/
class MedicalUserMailer extends BaseMailer implements MailerInterface
{
/**
* #todo To move into settings and inject via DI
* $from = $this->getParameter('medical.core.emails')['from']; LIKE in AdminController
*/
private $from = array('support#360medics.fr' => '360 medics');
/**
* #DI\InjectParams({
* "mailer"= #DI\Inject("mailer"),
* "router"= #DI\Inject("router"),
* "templating"= #DI\Inject("templating"),
* "config"= #DI\Inject("%medical.core.emails%")
* })
*/
public function __construct(
\Swift_Mailer $mailer, UrlGeneratorInterface $router, EngineInterface $templating, array $config
)
{
parent::__construct($mailer, $router, $templating, array());
// $this->mailer = $mailer;
// $this->router = $router;
// $this->templating = $templating;
$this->config = $config;
}
I'am new in testing PHP with PHPSpec. I have a class where i inject symfony current logged user (TokenStorageInterface). And make changes with that user.
<?php
namespace AppBundle\Service;
use AppBundle\Entity\Payment;
use AppBundle\Entity\User;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class TransferService
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
/**
* #var User
*/
private $currentUser;
/**
* #var InvoiceService
*/
private $invoiceService;
/**
* PaymentManager constructor.
* #param EntityManager $entityManager
* #param TokenStorageInterface $tokenStorage
* #param InvoiceService $invoiceService
*/
public function __construct(
EntityManager $entityManager,
TokenStorageInterface $tokenStorage,
InvoiceService $invoiceService
) {
$this->entityManager = $entityManager;
if ($tokenStorage->getToken() === null) {
throw new \Exception('User not logged in');
}
$this->currentUser = $tokenStorage->getToken()->getUser();
$this->invoiceService = $invoiceService;
}
/**
* #param Payment $payment
*/
public function transfer(Payment $payment)
{
$payer = $this->currentUser;
$amount = $payment->getAmount();
$receiver = $payment->getReceiver();
if ($payer === $receiver) {
throw new \LogicException('Cannot be same User');
}
if ($payer->getBalance() < $amount) {
throw new \LogicException('Not enough in balance');
}
$payment->setPayer($payer);
//TODO: Move to class?
$this->subtractBalance($payer, $amount);
$this->addBalance($receiver, $amount);
$this->invoiceService->createInvoice($payment);
$this->entityManager->persist($payment);
$this->entityManager->flush();
}
/**
* #param User $user
* #param $amount
*/
private function subtractBalance(User $user, $amount)
{
$user->setBalance($user->getBalance() - $amount);
}
/**
* #param User $user
* #param $amount
*/
private function addBalance(User $user, $amount)
{
$temp = $user->getBalance();
$user->setBalance($user->getBalance() + $amount);
}
}
And have wrote Spec for that class:
<?php
namespace spec\AppBundle\Service;
use AppBundle\Entity\Payment;
use AppBundle\Entity\User;
use AppBundle\Service\InvoiceService;
use Doctrine\ORM\EntityManager;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
class TransferServiceSpec extends ObjectBehavior
{
function let(EntityManager $entityManager, TokenStorage $tokenStorage, InvoiceService $invoiceService)
{
$user = new User();
$user->setUsername('aaa');
$user->setBalance(100.10);
$temp = new UsernamePasswordToken($user, null, 'main', ['ROLE_USER']);
$tokenStorage->getToken()->willReturn($temp);
$this->beConstructedWith($entityManager, $tokenStorage, $invoiceService);
}
function it_is_initializable()
{
$this->shouldHaveType('AppBundle\Service\TransferService');
}
function it_should_transfer_money(
User $user,
EntityManager $entityManager,
TokenStorageInterface $tokenStorage,
InvoiceService $invoiceService,
Payment $payment
) {
$user->getBalance()->willReturn(0);
$user->setBalance(99.9)->shouldBeCalled();
$payment->getReceiver()->willReturn($user);
//TODO how to check injected current user?
//$payment->getPayer()->willReturn($tokenStorage->getToken());
$payment->getAmount()->willReturn(99.9);
$invoiceService->createInvoice($payment)->shouldBeCalled();
$entityManager->persist($payment)->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
$this->transfer($payment);
}
}
The problem is, how to check that changes were made (to test that balance was edited) in current user (injected token storage getUser()) because following method dont work:
$payment->getPayer()->willReturn($tokenStorage->getToken()->getUser());
Call to undefined method Prophecy\Prophecy\MethodProphecy::getUser()
You should not call methods on prophecy, but mock everything instead, see:
function it_should_transfer_money(
User $user,
EntityManager $entityManager,
TokenStorageInterface $tokenStorage,
TokenInterface $token,
UserInterface $user,
InvoiceService $invoiceService,
Payment $payment
) {
$user->getBalance()->willReturn(0);
$user->setBalance(99.9)->shouldBeCalled();
$payment->getReceiver()->willReturn($user);
$tokenStorage->getToken()->willReturn($token);
$token->getUser()->willReturn($user);
$payment->getPayer()->willReturn($user);
$payment->getAmount()->willReturn(99.9);
$invoiceService->createInvoice($payment)->shouldBeCalled();
$entityManager->persist($payment)->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
$this->transfer($payment);
}