<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
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\AuthenticationException;
use Symfony\Component\Security\Core\Exception\SessionUnavailableException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* The AbstractAuthenticationListener is the preferred base class for all
* browser-/HTTP-based authentication requests.
*
* Subclasses likely have to implement the following:
* - an TokenInterface to hold authentication related data
* - an AuthenticationProvider to perform the actual authentication of the
* token, retrieve the UserInterface implementation from a database, and
* perform the specific account checks using the UserChecker
*
* By default, this listener only is active for a specific path, e.g.
* /login_check. If you want to change this behavior, you can overwrite the
* requiresAuthentication() method.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
abstract class AbstractAuthenticationListener extends AbstractListener
{
protected $options;
protected $logger;
protected $authenticationManager;
protected $providerKey;
protected $httpUtils;
private $tokenStorage;
private $sessionStrategy;
private $dispatcher;
private $successHandler;
private $failureHandler;
private $rememberMeServices;
/**
* @throws \InvalidArgumentException
*/
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, string $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = [], LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null)
{
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
$this->sessionStrategy = $sessionStrategy;
$this->providerKey = $providerKey;
$this->successHandler = $successHandler;
$this->failureHandler = $failureHandler;
$this->options = array_merge([
'check_path' => '/login_check',
'login_path' => '/login',
'always_use_default_target_path' => false,
'default_target_path' => '/',
'target_path_parameter' => '_target_path',
'use_referer' => false,
'failure_path' => null,
'failure_forward' => false,
'require_previous_session' => true,
], $options);
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->httpUtils = $httpUtils;
}
/**
* Sets the RememberMeServices implementation to use.
*/
public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices)
{
$this->rememberMeServices = $rememberMeServices;
}
/**
* {@inheritdoc}
*/
public function supports(Request $request): ?bool
{
return $this->requiresAuthentication($request);
}
/**
* Handles form based authentication.
*
* @throws \RuntimeException
* @throws SessionUnavailableException
*/
public function authenticate(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->hasSession()) {
throw new \RuntimeException('This authentication method requires a session.');
}
try {
if ($this->options['require_previous_session'] && !$request->hasPreviousSession()) {
throw new SessionUnavailableException('Your session has timed out, or you have disabled cookies.');
}
if (null === $returnValue = $this->attemptAuthentication($request)) {
return;
}
if ($returnValue instanceof TokenInterface) {
$this->sessionStrategy->onAuthentication($request, $returnValue);
$response = $this->onSuccess($request, $returnValue);
} elseif ($returnValue instanceof Response) {
$response = $returnValue;
} else {
throw new \RuntimeException('attemptAuthentication() must either return a Response, an implementation of TokenInterface, or null.');
}
} catch (AuthenticationException $e) {
$response = $this->onFailure($request, $e);
}
$event->setResponse($response);
}
/**
* Whether this request requires authentication.
*
* The default implementation only processes requests to a specific path,
* but a subclass could change this to only authenticate requests where a
* certain parameters is present.
*
* @return bool
*/
protected function requiresAuthentication(Request $request)
{
return $this->httpUtils->checkRequestPath($request, $this->options['check_path']);
}
/**
* Performs authentication.
*
* @return TokenInterface|Response|null The authenticated token, null if full authentication is not possible, or a Response
*
* @throws AuthenticationException if the authentication fails
*/
abstract protected function attemptAuthentication(Request $request);
private function onFailure(Request $request, AuthenticationException $failed): Response
{
if (null !== $this->logger) {
$this->logger->info('Authentication request failed.', ['exception' => $failed]);
}
$token = $this->tokenStorage->getToken();
if ($token instanceof UsernamePasswordToken && $this->providerKey === $token->getProviderKey()) {
$this->tokenStorage->setToken(null);
}
$response = $this->failureHandler->onAuthenticationFailure($request, $failed);
if (!$response instanceof Response) {
throw new \RuntimeException('Authentication Failure Handler did not return a Response.');
}
return $response;
}
private function onSuccess(Request $request, TokenInterface $token): Response
{
if (null !== $this->logger) {
$this->logger->info('User has been authenticated successfully.', ['username' => $token->getUsername()]);
}
$this->tokenStorage->setToken($token);
$session = $request->getSession();
$session->remove(Security::AUTHENTICATION_ERROR);
$session->remove(Security::LAST_USERNAME);
if (null !== $this->dispatcher) {
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->dispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
}
$response = $this->successHandler->onAuthenticationSuccess($request, $token);
if (!$response instanceof Response) {
throw new \RuntimeException('Authentication Success Handler did not return a Response.');
}
if (null !== $this->rememberMeServices) {
$this->rememberMeServices->loginSuccess($request, $response, $token);
}
return $response;
}
}