vendor/symfony/security-http/Firewall/SwitchUserListener.php line 40

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\HttpFoundation\RedirectResponse;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpKernel\Event\RequestEvent;
  15. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  16. use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
  17. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  18. use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
  19. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  20. use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
  21. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  22. use Symfony\Component\Security\Core\User\UserCheckerInterface;
  23. use Symfony\Component\Security\Core\User\UserInterface;
  24. use Symfony\Component\Security\Core\User\UserProviderInterface;
  25. use Symfony\Component\Security\Http\Event\SwitchUserEvent;
  26. use Symfony\Component\Security\Http\SecurityEvents;
  27. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  28. /**
  29. * SwitchUserListener allows a user to impersonate another one temporarily
  30. * (like the Unix su command).
  31. *
  32. * @author Fabien Potencier <fabien@symfony.com>
  33. *
  34. * @final
  35. */
  36. class SwitchUserListener extends AbstractListener
  37. {
  38. public const EXIT_VALUE = '_exit';
  39. private $tokenStorage;
  40. private $provider;
  41. private $userChecker;
  42. private $firewallName;
  43. private $accessDecisionManager;
  44. private $usernameParameter;
  45. private $role;
  46. private $logger;
  47. private $dispatcher;
  48. private $stateless;
  49. public function __construct(TokenStorageInterface $tokenStorage, UserProviderInterface $provider, UserCheckerInterface $userChecker, string $firewallName, AccessDecisionManagerInterface $accessDecisionManager, ?LoggerInterface $logger = null, string $usernameParameter = '_switch_user', string $role = 'ROLE_ALLOWED_TO_SWITCH', ?EventDispatcherInterface $dispatcher = null, bool $stateless = false)
  50. {
  51. if ('' === $firewallName) {
  52. throw new \InvalidArgumentException('$firewallName must not be empty.');
  53. }
  54. $this->tokenStorage = $tokenStorage;
  55. $this->provider = $provider;
  56. $this->userChecker = $userChecker;
  57. $this->firewallName = $firewallName;
  58. $this->accessDecisionManager = $accessDecisionManager;
  59. $this->usernameParameter = $usernameParameter;
  60. $this->role = $role;
  61. $this->logger = $logger;
  62. $this->dispatcher = $dispatcher;
  63. $this->stateless = $stateless;
  64. }
  65. /**
  66. * {@inheritdoc}
  67. */
  68. public function supports(Request $request): ?bool
  69. {
  70. // usernames can be falsy
  71. $username = $request->get($this->usernameParameter);
  72. if (null === $username || '' === $username) {
  73. $username = $request->headers->get($this->usernameParameter);
  74. }
  75. // if it's still "empty", nothing to do.
  76. if (null === $username || '' === $username) {
  77. return false;
  78. }
  79. $request->attributes->set('_switch_user_username', $username);
  80. return true;
  81. }
  82. /**
  83. * Handles the switch to another user.
  84. *
  85. * @throws \LogicException if switching to a user failed
  86. */
  87. public function authenticate(RequestEvent $event)
  88. {
  89. $request = $event->getRequest();
  90. $username = $request->attributes->get('_switch_user_username');
  91. $request->attributes->remove('_switch_user_username');
  92. if (null === $this->tokenStorage->getToken()) {
  93. throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
  94. }
  95. if (self::EXIT_VALUE === $username) {
  96. $this->attemptExitUser($request);
  97. } else {
  98. try {
  99. $this->tokenStorage->setToken($this->attemptSwitchUser($request, $username));
  100. } catch (AuthenticationException $e) {
  101. // Generate 403 in any conditions to prevent user enumeration vulnerabilities
  102. throw new AccessDeniedException('Switch User failed: '.$e->getMessage(), $e);
  103. }
  104. }
  105. if (!$this->stateless) {
  106. $request->query->remove($this->usernameParameter);
  107. $request->server->set('QUERY_STRING', http_build_query($request->query->all(), '', '&'));
  108. $response = new RedirectResponse($request->getUri(), 302);
  109. $event->setResponse($response);
  110. }
  111. }
  112. /**
  113. * Attempts to switch to another user and returns the new token if successfully switched.
  114. *
  115. * @throws \LogicException
  116. * @throws AccessDeniedException
  117. */
  118. private function attemptSwitchUser(Request $request, string $username): ?TokenInterface
  119. {
  120. $token = $this->tokenStorage->getToken();
  121. $originalToken = $this->getOriginalToken($token);
  122. if (null !== $originalToken) {
  123. // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0
  124. if ((method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername()) === $username) {
  125. return $token;
  126. }
  127. // User already switched, exit before seamlessly switching to another user
  128. $token = $this->attemptExitUser($request);
  129. }
  130. // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0
  131. $currentUsername = method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername();
  132. $nonExistentUsername = '_'.md5(random_bytes(8).$username);
  133. // To protect against user enumeration via timing measurements
  134. // we always load both successfully and unsuccessfully
  135. $methodName = 'loadUserByIdentifier';
  136. if (!method_exists($this->provider, $methodName)) {
  137. trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->provider));
  138. $methodName = 'loadUserByUsername';
  139. }
  140. try {
  141. $user = $this->provider->$methodName($username);
  142. try {
  143. $this->provider->$methodName($nonExistentUsername);
  144. } catch (\Exception $e) {
  145. }
  146. } catch (AuthenticationException $e) {
  147. $this->provider->$methodName($currentUsername);
  148. throw $e;
  149. }
  150. if (false === $this->accessDecisionManager->decide($token, [$this->role], $user)) {
  151. $exception = new AccessDeniedException();
  152. $exception->setAttributes($this->role);
  153. throw $exception;
  154. }
  155. if (null !== $this->logger) {
  156. $this->logger->info('Attempting to switch to user.', ['username' => $username]);
  157. }
  158. $this->userChecker->checkPostAuth($user);
  159. $roles = $user->getRoles();
  160. $roles[] = 'ROLE_PREVIOUS_ADMIN';
  161. $originatedFromUri = str_replace('/&', '/?', preg_replace('#[&?]'.$this->usernameParameter.'=[^&]*#', '', $request->getRequestUri()));
  162. $token = new SwitchUserToken($user, $this->firewallName, $roles, $token, $originatedFromUri);
  163. if (null !== $this->dispatcher) {
  164. $switchEvent = new SwitchUserEvent($request, $token->getUser(), $token);
  165. $this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER);
  166. // use the token from the event in case any listeners have replaced it.
  167. $token = $switchEvent->getToken();
  168. }
  169. return $token;
  170. }
  171. /**
  172. * Attempts to exit from an already switched user and returns the original token.
  173. *
  174. * @throws AuthenticationCredentialsNotFoundException
  175. */
  176. private function attemptExitUser(Request $request): TokenInterface
  177. {
  178. if (null === ($currentToken = $this->tokenStorage->getToken()) || null === $original = $this->getOriginalToken($currentToken)) {
  179. throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
  180. }
  181. if (null !== $this->dispatcher && $original->getUser() instanceof UserInterface) {
  182. $user = $this->provider->refreshUser($original->getUser());
  183. $original->setUser($user);
  184. $switchEvent = new SwitchUserEvent($request, $user, $original);
  185. $this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER);
  186. $original = $switchEvent->getToken();
  187. }
  188. $this->tokenStorage->setToken($original);
  189. return $original;
  190. }
  191. private function getOriginalToken(TokenInterface $token): ?TokenInterface
  192. {
  193. if ($token instanceof SwitchUserToken) {
  194. return $token->getOriginalToken();
  195. }
  196. return null;
  197. }
  198. }