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

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