vendor/ibexa/rest/src/lib/Server/Security/RestAuthenticator.php line 35

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