<?php
/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
namespace Ibexa\Rest\Server\Security;
use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
use Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface;
use Ibexa\Core\MVC\Symfony\Security\UserInterface as IbexaUser;
use Ibexa\Rest\Server\Exceptions\InvalidUserTypeException;
use Ibexa\Rest\Server\Exceptions\UserConflictException;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
use Symfony\Component\Security\Http\Logout\SessionLogoutHandler;
use Symfony\Component\Security\Http\SecurityEvents;
/**
* Authenticator for REST API, mainly used for session based authentication (session creation resource).
*
* Implements \Symfony\Component\Security\Http\Firewall\ListenerInterface to be able to receive the provider key
* (firewall identifier from configuration).
*/
class RestAuthenticator implements AuthenticatorInterface
{
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @var \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface
*/
private $authenticationManager;
/**
* @var string
*/
private $providerKey;
/**
* @var \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface
*/
private $tokenStorage;
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
private $dispatcher;
/**
* @var \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface
*/
private $configResolver;
/**
* @var \Symfony\Component\Security\Http\Logout\LogoutHandlerInterface[]
*/
private $logoutHandlers = [];
public function __construct(
TokenStorageInterface $tokenStorage,
AuthenticationManagerInterface $authenticationManager,
$providerKey,
EventDispatcherInterface $dispatcher,
ConfigResolverInterface $configResolver,
LoggerInterface $logger = null
) {
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
$this->providerKey = $providerKey;
$this->dispatcher = $dispatcher;
$this->configResolver = $configResolver;
$this->logger = $logger;
}
/**
* Doesn't do anything as we don't use this service with main Firewall listener.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
*/
public function __invoke(RequestEvent $event)
{
return;
}
public function authenticate(Request $request)
{
// If a token already exists and username is the same as the one we request authentication for,
// then return it and mark it as coming from session.
$previousToken = $this->tokenStorage->getToken();
if (
$previousToken instanceof TokenInterface
&& $previousToken->getUsername() === $request->attributes->get('username')
) {
$previousToken->setAttribute('isFromSession', true);
return $previousToken;
}
$token = $this->attemptAuthentication($request);
if (!$token instanceof TokenInterface) {
if ($this->logger) {
$this->logger->error('REST: No token could be found in SecurityContext');
}
throw new TokenNotFoundException();
}
$this->tokenStorage->setToken($token);
$this->dispatcher->dispatch(new InteractiveLoginEvent($request, $token), SecurityEvents::INTERACTIVE_LOGIN);
// Re-fetch token from SecurityContext since an INTERACTIVE_LOGIN listener might have changed it
// i.e. when using multiple user providers.
// @see \Ibexa\Core\MVC\Symfony\Security\EventListener\SecurityListener::onInteractiveLogin()
$token = $this->tokenStorage->getToken();
$user = $token->getUser();
if (!$user instanceof IbexaUser) {
if ($this->logger) {
$this->logger->error('REST: Authenticated user must be Ibexa\\Core\\MVC\\Symfony\\Security\\User, got ' . is_string($user) ? $user : get_class($user));
}
$e = new InvalidUserTypeException('Authenticated user is not an Ibexa User.');
$e->setToken($token);
throw $e;
}
// Check if newly logged in user differs from previous one.
if ($this->isUserConflict($user, $previousToken)) {
$this->tokenStorage->setToken($previousToken);
throw new UserConflictException();
}
return $token;
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
*
* @return \Symfony\Component\Security\Core\Authentication\Token\TokenInterface
*/
private function attemptAuthentication(Request $request)
{
return $this->authenticationManager->authenticate(
new UsernamePasswordToken(
$request->attributes->get('username'),
$request->attributes->get('password'),
$this->providerKey
)
);
}
/**
* Checks if newly matched user is conflicting with previously non-anonymous logged in user, if any.
*
* @param \Ibexa\Core\MVC\Symfony\Security\UserInterface $user
* @param \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $previousToken
*
* @return bool
*/
private function isUserConflict(IbexaUser $user, TokenInterface $previousToken = null)
{
if ($previousToken === null || !$previousToken instanceof UsernamePasswordToken) {
return false;
}
$previousUser = $previousToken->getUser();
if (!$previousUser instanceof IbexaUser) {
return false;
}
$wasAnonymous = $previousUser->getAPIUser()->getUserId() == $this->configResolver->getParameter('anonymous_user_id');
// TODO: isEqualTo is not on the interface
return !$wasAnonymous && !$user->isEqualTo($previousUser);
}
public function addLogoutHandler(LogoutHandlerInterface $handler)
{
$this->logoutHandlers[] = $handler;
}
public function logout(Request $request)
{
$response = new Response();
// Manually clear the session through session storage.
// Session::invalidate() is not called on purpose, to avoid unwanted session migration that would imply
// generation of a new session id.
// REST logout must indeed clear the session cookie.
// See \Ibexa\Rest\Server\Security\RestLogoutHandler
$request->getSession()->clear();
$token = $this->tokenStorage->getToken();
foreach ($this->logoutHandlers as $handler) {
// Explicitly ignore SessionLogoutHandler as we do session invalidation manually here,
// through the session storage, to avoid unwanted session migration.
if ($handler instanceof SessionLogoutHandler) {
continue;
}
$handler->logout($request, $response, $token);
}
return $response;
}
}
class_alias(RestAuthenticator::class, 'EzSystems\EzPlatformRest\Server\Security\RestAuthenticator');