vendor/ezsystems/ezpublish-kernel/eZ/Publish/Core/REST/Server/Security/RestAuthenticator.php line 37

Open in your IDE?
  1. <?php
  2. /**
  3.  * @copyright Copyright (C) eZ Systems AS. All rights reserved.
  4.  * @license For full copyright and license information view LICENSE file distributed with this source code.
  5.  */
  6. namespace eZ\Publish\Core\REST\Server\Security;
  7. use eZ\Publish\Core\MVC\ConfigResolverInterface;
  8. use eZ\Publish\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface;
  9. use eZ\Publish\Core\MVC\Symfony\Security\UserInterface as EzUser;
  10. use eZ\Publish\Core\REST\Server\Exceptions\InvalidUserTypeException;
  11. use eZ\Publish\Core\REST\Server\Exceptions\UserConflictException;
  12. use Psr\Log\LoggerInterface;
  13. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\HttpFoundation\Response;
  16. use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
  17. use Symfony\Component\HttpKernel\Event\GetResponseEvent;
  18. use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
  19. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  20. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  21. use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
  22. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  23. use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
  24. use Symfony\Component\Security\Http\Firewall\ListenerInterface;
  25. use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
  26. use Symfony\Component\Security\Http\Logout\SessionLogoutHandler;
  27. use Symfony\Component\Security\Http\SecurityEvents;
  28. /**
  29.  * Authenticator for REST API, mainly used for session based authentication (session creation resource).
  30.  *
  31.  * Implements \Symfony\Component\Security\Http\Firewall\ListenerInterface to be able to receive the provider key
  32.  * (firewall identifier from configuration).
  33.  */
  34. class RestAuthenticator implements ListenerInterfaceAuthenticatorInterface
  35. {
  36.     /** @var \Psr\Log\LoggerInterface */
  37.     private $logger;
  38.     /** @var \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface */
  39.     private $authenticationManager;
  40.     /** @var string */
  41.     private $providerKey;
  42.     /** @var \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface */
  43.     private $tokenStorage;
  44.     /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */
  45.     private $dispatcher;
  46.     /** @var \eZ\Publish\Core\MVC\ConfigResolverInterface */
  47.     private $configResolver;
  48.     /** @var \Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface */
  49.     private $sessionStorage;
  50.     /** @var \Symfony\Component\Security\Http\Logout\LogoutHandlerInterface[] */
  51.     private $logoutHandlers = [];
  52.     public function __construct(
  53.         TokenStorageInterface $tokenStorage,
  54.         AuthenticationManagerInterface $authenticationManager,
  55.         $providerKey,
  56.         EventDispatcherInterface $dispatcher,
  57.         ConfigResolverInterface $configResolver,
  58.         SessionStorageInterface $sessionStorage,
  59.         LoggerInterface $logger null
  60.     ) {
  61.         $this->tokenStorage $tokenStorage;
  62.         $this->authenticationManager $authenticationManager;
  63.         $this->providerKey $providerKey;
  64.         $this->dispatcher $dispatcher;
  65.         $this->configResolver $configResolver;
  66.         $this->sessionStorage $sessionStorage;
  67.         $this->logger $logger;
  68.     }
  69.     /**
  70.      * Doesn't do anything as we don't use this service with main Firewall listener.
  71.      *
  72.      * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
  73.      */
  74.     public function handle(GetResponseEvent $event)
  75.     {
  76.         return;
  77.     }
  78.     public function authenticate(Request $request)
  79.     {
  80.         // If a token already exists and username is the same as the one we request authentication for,
  81.         // then return it and mark it as coming from session.
  82.         $previousToken $this->tokenStorage->getToken();
  83.         if (
  84.             $previousToken instanceof TokenInterface
  85.             && $previousToken->getUsername() === $request->attributes->get('username')
  86.         ) {
  87.             $previousToken->setAttribute('isFromSession'true);
  88.             return $previousToken;
  89.         }
  90.         $token $this->attemptAuthentication($request);
  91.         if (!$token instanceof TokenInterface) {
  92.             if ($this->logger) {
  93.                 $this->logger->error('REST: No token could be found in SecurityContext');
  94.             }
  95.             throw new TokenNotFoundException();
  96.         }
  97.         $this->tokenStorage->setToken($token);
  98.         $this->dispatcher->dispatch(
  99.             SecurityEvents::INTERACTIVE_LOGIN,
  100.             new InteractiveLoginEvent($request$token)
  101.         );
  102.         // Re-fetch token from SecurityContext since an INTERACTIVE_LOGIN listener might have changed it
  103.         // i.e. when using multiple user providers.
  104.         // @see \eZ\Publish\Core\MVC\Symfony\Security\EventListener\SecurityListener::onInteractiveLogin()
  105.         $token $this->tokenStorage->getToken();
  106.         $user $token->getUser();
  107.         if (!$user instanceof EzUser) {
  108.             if ($this->logger) {
  109.                 $this->logger->error('REST: Authenticated user must be eZ\Publish\Core\MVC\Symfony\Security\User, got ' is_string($user) ? $user get_class($user));
  110.             }
  111.             $e = new InvalidUserTypeException('Authenticated user is not an eZ User.');
  112.             $e->setToken($token);
  113.             throw $e;
  114.         }
  115.         // Check if newly logged in user differs from previous one.
  116.         if ($this->isUserConflict($user$previousToken)) {
  117.             $this->tokenStorage->setToken($previousToken);
  118.             throw new UserConflictException();
  119.         }
  120.         return $token;
  121.     }
  122.     /**
  123.      * @param \Symfony\Component\HttpFoundation\Request $request
  124.      *
  125.      * @return \Symfony\Component\Security\Core\Authentication\Token\TokenInterface
  126.      */
  127.     private function attemptAuthentication(Request $request)
  128.     {
  129.         return $this->authenticationManager->authenticate(
  130.             new UsernamePasswordToken(
  131.                 $request->attributes->get('username'),
  132.                 $request->attributes->get('password'),
  133.                 $this->providerKey
  134.             )
  135.         );
  136.     }
  137.     /**
  138.      * Checks if newly matched user is conflicting with previously non-anonymous logged in user, if any.
  139.      *
  140.      * @param EzUser $user
  141.      * @param TokenInterface $previousToken
  142.      *
  143.      * @return bool
  144.      */
  145.     private function isUserConflict(EzUser $userTokenInterface $previousToken null)
  146.     {
  147.         if ($previousToken === null || !$previousToken instanceof UsernamePasswordToken) {
  148.             return false;
  149.         }
  150.         $previousUser $previousToken->getUser();
  151.         if (!$previousUser instanceof EzUser) {
  152.             return false;
  153.         }
  154.         $wasAnonymous $previousUser->getAPIUser()->getUserId() == $this->configResolver->getParameter('anonymous_user_id');
  155.         // TODO: isEqualTo is not on the interface
  156.         return !$wasAnonymous && !$user->isEqualTo($previousUser);
  157.     }
  158.     public function addLogoutHandler(LogoutHandlerInterface $handler)
  159.     {
  160.         $this->logoutHandlers[] = $handler;
  161.     }
  162.     public function logout(Request $request)
  163.     {
  164.         $response = new Response();
  165.         // Manually clear the session through session storage.
  166.         // Session::invalidate() is not called on purpose, to avoid unwanted session migration that would imply
  167.         // generation of a new session id.
  168.         // REST logout must indeed clear the session cookie.
  169.         // See \eZ\Publish\Core\REST\Server\Security\RestLogoutHandler
  170.         $this->sessionStorage->clear();
  171.         $token $this->tokenStorage->getToken();
  172.         foreach ($this->logoutHandlers as $handler) {
  173.             // Explicitly ignore SessionLogoutHandler as we do session invalidation manually here,
  174.             // through the session storage, to avoid unwanted session migration.
  175.             if ($handler instanceof SessionLogoutHandler) {
  176.                 continue;
  177.             }
  178.             $handler->logout($request$response$token);
  179.         }
  180.         return $response;
  181.     }
  182. }