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 $tokenStorage, iterable $userProviders, string $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->sessionKey, serialize($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($refreshedUser, false);
  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(false, false);
  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, $newToken, false), 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($msg, 0x37313BC, $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($originalUser, TokenInterface $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: '.$class, 0x37313BC);
  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. }