vendor/symfony/security-http/Firewall/ContextListener.php line 170

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Security\Http\Firewall;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\EventDispatcher\Event;
  13. use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\HttpFoundation\Session\Session;
  16. use Symfony\Component\HttpKernel\Event\RequestEvent;
  17. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  18. use Symfony\Component\HttpKernel\KernelEvents;
  19. use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
  20. use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
  21. use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
  22. use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
  23. use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
  24. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  25. use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
  26. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  27. use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
  28. use Symfony\Component\Security\Core\Exception\UserNotFoundException;
  29. use Symfony\Component\Security\Core\User\EquatableInterface;
  30. use Symfony\Component\Security\Core\User\UserInterface;
  31. use Symfony\Component\Security\Core\User\UserProviderInterface;
  32. use Symfony\Component\Security\Http\Event\DeauthenticatedEvent;
  33. use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
  34. use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
  35. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  36. /**
  37.  * ContextListener manages the SecurityContext persistence through a session.
  38.  *
  39.  * @author Fabien Potencier <fabien@symfony.com>
  40.  * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  41.  *
  42.  * @final
  43.  */
  44. class ContextListener extends AbstractListener
  45. {
  46.     private $tokenStorage;
  47.     private $sessionKey;
  48.     private $logger;
  49.     private $userProviders;
  50.     private $dispatcher;
  51.     private $registered;
  52.     private $trustResolver;
  53.     private $rememberMeServices;
  54.     private $sessionTrackerEnabler;
  55.     /**
  56.      * @param iterable<mixed, UserProviderInterface> $userProviders
  57.      */
  58.     public function __construct(TokenStorageInterface $tokenStorageiterable $userProvidersstring $contextKey, ?LoggerInterface $logger null, ?EventDispatcherInterface $dispatcher null, ?AuthenticationTrustResolverInterface $trustResolver null, ?callable $sessionTrackerEnabler null)
  59.     {
  60.         if (empty($contextKey)) {
  61.             throw new \InvalidArgumentException('$contextKey must not be empty.');
  62.         }
  63.         $this->tokenStorage $tokenStorage;
  64.         $this->userProviders $userProviders;
  65.         $this->sessionKey '_security_'.$contextKey;
  66.         $this->logger $logger;
  67.         $this->dispatcher class_exists(Event::class) ? LegacyEventDispatcherProxy::decorate($dispatcher) : $dispatcher;
  68.         $this->trustResolver $trustResolver ?? new AuthenticationTrustResolver(AnonymousToken::class, RememberMeToken::class);
  69.         $this->sessionTrackerEnabler $sessionTrackerEnabler;
  70.     }
  71.     /**
  72.      * {@inheritdoc}
  73.      */
  74.     public function supports(Request $request): ?bool
  75.     {
  76.         return null// always run authenticate() lazily with lazy firewalls
  77.     }
  78.     /**
  79.      * Reads the Security Token from the session.
  80.      */
  81.     public function authenticate(RequestEvent $event)
  82.     {
  83.         if (!$this->registered && null !== $this->dispatcher && $event->isMainRequest()) {
  84.             $this->dispatcher->addListener(KernelEvents::RESPONSE, [$this'onKernelResponse']);
  85.             $this->registered true;
  86.         }
  87.         $request $event->getRequest();
  88.         $session = !$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession() && $request->hasSession() ? $request->getSession() : null;
  89.         $request->attributes->set('_security_firewall_run'$this->sessionKey);
  90.         if (null !== $session) {
  91.             $usageIndexValue $session instanceof Session $usageIndexReference = &$session->getUsageIndex() : 0;
  92.             $usageIndexReference = \PHP_INT_MIN;
  93.             $sessionId $request->cookies->all()[$session->getName()] ?? null;
  94.             $token $session->get($this->sessionKey);
  95.             // sessionId = true is used in the tests
  96.             if ($this->sessionTrackerEnabler && \in_array($sessionId, [true$session->getId()], true)) {
  97.                 $usageIndexReference $usageIndexValue;
  98.             } else {
  99.                 $usageIndexReference $usageIndexReference - \PHP_INT_MIN $usageIndexValue;
  100.             }
  101.         }
  102.         if (null === $session || null === $token) {
  103.             if ($this->sessionTrackerEnabler) {
  104.                 ($this->sessionTrackerEnabler)();
  105.             }
  106.             $this->tokenStorage->setToken(null);
  107.             return;
  108.         }
  109.         $token $this->safelyUnserialize($token);
  110.         if (null !== $this->logger) {
  111.             $this->logger->debug('Read existing security token from the session.', [
  112.                 'key' => $this->sessionKey,
  113.                 'token_class' => \is_object($token) ? \get_class($token) : null,
  114.             ]);
  115.         }
  116.         if ($token instanceof TokenInterface) {
  117.             $originalToken $token;
  118.             $token $this->refreshUser($token);
  119.             if (!$token) {
  120.                 if ($this->logger) {
  121.                     $this->logger->debug('Token was deauthenticated after trying to refresh it.');
  122.                 }
  123.                 if ($this->dispatcher) {
  124.                     $this->dispatcher->dispatch(new TokenDeauthenticatedEvent($originalToken$request));
  125.                 }
  126.                 if ($this->rememberMeServices) {
  127.                     $this->rememberMeServices->loginFail($request);
  128.                 }
  129.             }
  130.         } elseif (null !== $token) {
  131.             if (null !== $this->logger) {
  132.                 $this->logger->warning('Expected a security token from the session, got something else.', ['key' => $this->sessionKey'received' => $token]);
  133.             }
  134.             $token null;
  135.         }
  136.         if ($this->sessionTrackerEnabler) {
  137.             ($this->sessionTrackerEnabler)();
  138.         }
  139.         $this->tokenStorage->setToken($token);
  140.     }
  141.     /**
  142.      * Writes the security token into the session.
  143.      */
  144.     public function onKernelResponse(ResponseEvent $event)
  145.     {
  146.         if (!$event->isMainRequest()) {
  147.             return;
  148.         }
  149.         $request $event->getRequest();
  150.         if (!$request->hasSession() || $request->attributes->get('_security_firewall_run') !== $this->sessionKey) {
  151.             return;
  152.         }
  153.         if ($this->dispatcher) {
  154.             $this->dispatcher->removeListener(KernelEvents::RESPONSE, [$this'onKernelResponse']);
  155.         }
  156.         $this->registered false;
  157.         $session $request->getSession();
  158.         $sessionId $session->getId();
  159.         $usageIndexValue $session instanceof Session $usageIndexReference = &$session->getUsageIndex() : null;
  160.         $token $this->tokenStorage->getToken();
  161.         // @deprecated always use isAuthenticated() in 6.0
  162.         $notAuthenticated method_exists($this->trustResolver'isAuthenticated') ? !$this->trustResolver->isAuthenticated($token) : (null === $token || $this->trustResolver->isAnonymous($token));
  163.         if ($notAuthenticated) {
  164.             if ($request->hasPreviousSession()) {
  165.                 $session->remove($this->sessionKey);
  166.             }
  167.         } else {
  168.             $session->set($this->sessionKeyserialize($token));
  169.             if (null !== $this->logger) {
  170.                 $this->logger->debug('Stored the security token in the session.', ['key' => $this->sessionKey]);
  171.             }
  172.         }
  173.         if ($this->sessionTrackerEnabler && $session->getId() === $sessionId) {
  174.             $usageIndexReference $usageIndexValue;
  175.         }
  176.     }
  177.     /**
  178.      * Refreshes the user by reloading it from the user provider.
  179.      *
  180.      * @throws \RuntimeException
  181.      */
  182.     protected function refreshUser(TokenInterface $token): ?TokenInterface
  183.     {
  184.         $user $token->getUser();
  185.         if (!$user instanceof UserInterface) {
  186.             return $token;
  187.         }
  188.         $userNotFoundByProvider false;
  189.         $userDeauthenticated false;
  190.         $userClass = \get_class($user);
  191.         foreach ($this->userProviders as $provider) {
  192.             if (!$provider instanceof UserProviderInterface) {
  193.                 throw new \InvalidArgumentException(sprintf('User provider "%s" must implement "%s".'get_debug_type($provider), UserProviderInterface::class));
  194.             }
  195.             if (!$provider->supportsClass($userClass)) {
  196.                 continue;
  197.             }
  198.             try {
  199.                 $refreshedUser $provider->refreshUser($user);
  200.                 $newToken = clone $token;
  201.                 $newToken->setUser($refreshedUserfalse);
  202.                 // tokens can be deauthenticated if the user has been changed.
  203.                 if ($token instanceof AbstractToken && $this->hasUserChanged($user$newToken)) {
  204.                     $userDeauthenticated true;
  205.                     // @deprecated since Symfony 5.4
  206.                     if (method_exists($newToken'setAuthenticated')) {
  207.                         $newToken->setAuthenticated(falsefalse);
  208.                     }
  209.                     if (null !== $this->logger) {
  210.                         // @deprecated since Symfony 5.3, change to $refreshedUser->getUserIdentifier() in 6.0
  211.                         $this->logger->debug('Cannot refresh token because user has changed.', ['username' => method_exists($refreshedUser'getUserIdentifier') ? $refreshedUser->getUserIdentifier() : $refreshedUser->getUsername(), 'provider' => \get_class($provider)]);
  212.                     }
  213.                     continue;
  214.                 }
  215.                 $token->setUser($refreshedUser);
  216.                 if (null !== $this->logger) {
  217.                     // @deprecated since Symfony 5.3, change to $refreshedUser->getUserIdentifier() in 6.0
  218.                     $context = ['provider' => \get_class($provider), 'username' => method_exists($refreshedUser'getUserIdentifier') ? $refreshedUser->getUserIdentifier() : $refreshedUser->getUsername()];
  219.                     if ($token instanceof SwitchUserToken) {
  220.                         $originalToken $token->getOriginalToken();
  221.                         // @deprecated since Symfony 5.3, change to $originalToken->getUserIdentifier() in 6.0
  222.                         $context['impersonator_username'] = method_exists($originalToken'getUserIdentifier') ? $originalToken->getUserIdentifier() : $originalToken->getUsername();
  223.                     }
  224.                     $this->logger->debug('User was reloaded from a user provider.'$context);
  225.                 }
  226.                 return $token;
  227.             } catch (UnsupportedUserException $e) {
  228.                 // let's try the next user provider
  229.             } catch (UserNotFoundException $e) {
  230.                 if (null !== $this->logger) {
  231.                     $this->logger->warning('Username could not be found in the selected user provider.', ['username' => method_exists($e'getUserIdentifier') ? $e->getUserIdentifier() : $e->getUsername(), 'provider' => \get_class($provider)]);
  232.                 }
  233.                 $userNotFoundByProvider true;
  234.             }
  235.         }
  236.         if ($userDeauthenticated) {
  237.             // @deprecated since Symfony 5.4
  238.             if ($this->dispatcher) {
  239.                 $this->dispatcher->dispatch(new DeauthenticatedEvent($token$newTokenfalse), DeauthenticatedEvent::class);
  240.             }
  241.             return null;
  242.         }
  243.         if ($userNotFoundByProvider) {
  244.             return null;
  245.         }
  246.         throw new \RuntimeException(sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?'$userClass));
  247.     }
  248.     private function safelyUnserialize(string $serializedToken)
  249.     {
  250.         $token null;
  251.         $prevUnserializeHandler ini_set('unserialize_callback_func'__CLASS__.'::handleUnserializeCallback');
  252.         $prevErrorHandler set_error_handler(function ($type$msg$file$line$context = []) use (&$prevErrorHandler) {
  253.             if (__FILE__ === $file && !\in_array($type, [\E_DEPRECATED, \E_USER_DEPRECATED], true)) {
  254.                 throw new \ErrorException($msg0x37313BC$type$file$line);
  255.             }
  256.             return $prevErrorHandler $prevErrorHandler($type$msg$file$line$context) : false;
  257.         });
  258.         try {
  259.             $token unserialize($serializedToken);
  260.         } catch (\ErrorException $e) {
  261.             if (0x37313BC !== $e->getCode()) {
  262.                 throw $e;
  263.             }
  264.             if ($this->logger) {
  265.                 $this->logger->warning('Failed to unserialize the security token from the session.', ['key' => $this->sessionKey'received' => $serializedToken'exception' => $e]);
  266.             }
  267.         } finally {
  268.             restore_error_handler();
  269.             ini_set('unserialize_callback_func'$prevUnserializeHandler);
  270.         }
  271.         return $token;
  272.     }
  273.     /**
  274.      * @param string|\Stringable|UserInterface $originalUser
  275.      */
  276.     private static function hasUserChanged($originalUserTokenInterface $refreshedToken): bool
  277.     {
  278.         $refreshedUser $refreshedToken->getUser();
  279.         if ($originalUser instanceof UserInterface) {
  280.             if (!$refreshedUser instanceof UserInterface) {
  281.                 return true;
  282.             } else {
  283.                 // noop
  284.             }
  285.         } elseif ($refreshedUser instanceof UserInterface) {
  286.             return true;
  287.         } else {
  288.             return (string) $originalUser !== (string) $refreshedUser;
  289.         }
  290.         if ($originalUser instanceof EquatableInterface) {
  291.             return !(bool) $originalUser->isEqualTo($refreshedUser);
  292.         }
  293.         // @deprecated since Symfony 5.3, check for PasswordAuthenticatedUserInterface on both user objects before comparing passwords
  294.         if ($originalUser->getPassword() !== $refreshedUser->getPassword()) {
  295.             return true;
  296.         }
  297.         // @deprecated since Symfony 5.3, check for LegacyPasswordAuthenticatedUserInterface on both user objects before comparing salts
  298.         if ($originalUser->getSalt() !== $refreshedUser->getSalt()) {
  299.             return true;
  300.         }
  301.         $userRoles array_map('strval', (array) $refreshedUser->getRoles());
  302.         if ($refreshedToken instanceof SwitchUserToken) {
  303.             $userRoles[] = 'ROLE_PREVIOUS_ADMIN';
  304.         }
  305.         if (
  306.             \count($userRoles) !== \count($refreshedToken->getRoleNames()) ||
  307.             \count($userRoles) !== \count(array_intersect($userRoles$refreshedToken->getRoleNames()))
  308.         ) {
  309.             return true;
  310.         }
  311.         // @deprecated since Symfony 5.3, drop getUsername() in 6.0
  312.         $userIdentifier = function ($refreshedUser) {
  313.             return method_exists($refreshedUser'getUserIdentifier') ? $refreshedUser->getUserIdentifier() : $refreshedUser->getUsername();
  314.         };
  315.         if ($userIdentifier($originalUser) !== $userIdentifier($refreshedUser)) {
  316.             return true;
  317.         }
  318.         return false;
  319.     }
  320.     /**
  321.      * @internal
  322.      */
  323.     public static function handleUnserializeCallback(string $class)
  324.     {
  325.         throw new \ErrorException('Class not found: '.$class0x37313BC);
  326.     }
  327.     /**
  328.      * @deprecated since Symfony 5.4
  329.      */
  330.     public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices)
  331.     {
  332.         trigger_deprecation('symfony/security-http''5.4''Method "%s()" is deprecated, use the new remember me handlers instead.'__METHOD__);
  333.         $this->rememberMeServices $rememberMeServices;
  334.     }
  335. }