<?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\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
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\BadCredentialsException;
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\SecurityEvents;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* UsernamePasswordJsonAuthenticationListener is a stateless implementation of
* an authentication via a JSON document composed of a username and a password.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class UsernamePasswordJsonAuthenticationListener extends AbstractListener
{
private $tokenStorage;
private $authenticationManager;
private $httpUtils;
private $providerKey;
private $successHandler;
private $failureHandler;
private $options;
private $logger;
private $eventDispatcher;
private $propertyAccessor;
private $sessionStrategy;
/**
* @var TranslatorInterface|null
*/
private $translator;
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, HttpUtils $httpUtils, string $providerKey, AuthenticationSuccessHandlerInterface $successHandler = null, AuthenticationFailureHandlerInterface $failureHandler = null, array $options = [], LoggerInterface $logger = null, EventDispatcherInterface $eventDispatcher = null, PropertyAccessorInterface $propertyAccessor = null)
{
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
$this->httpUtils = $httpUtils;
$this->providerKey = $providerKey;
$this->successHandler = $successHandler;
$this->failureHandler = $failureHandler;
$this->logger = $logger;
$this->eventDispatcher = $eventDispatcher;
$this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options);
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
public function supports(Request $request): ?bool
{
if (false === strpos($request->getRequestFormat(), 'json')
&& false === strpos($request->getContentType(), 'json')
) {
return false;
}
if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestEvent $event)
{
$request = $event->getRequest();
$data = json_decode($request->getContent());
try {
if (!$data instanceof \stdClass) {
throw new BadRequestHttpException('Invalid JSON.');
}
try {
$username = $this->propertyAccessor->getValue($data, $this->options['username_path']);
} catch (AccessException $e) {
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['username_path']), $e);
}
try {
$password = $this->propertyAccessor->getValue($data, $this->options['password_path']);
} catch (AccessException $e) {
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
}
if (!\is_string($username)) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['username_path']));
}
if (\strlen($username) > Security::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Invalid username.');
}
if (!\is_string($password)) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['password_path']));
}
$token = new UsernamePasswordToken($username, $password, $this->providerKey);
$authenticatedToken = $this->authenticationManager->authenticate($token);
$response = $this->onSuccess($request, $authenticatedToken);
} catch (AuthenticationException $e) {
$response = $this->onFailure($request, $e);
} catch (BadRequestHttpException $e) {
$request->setRequestFormat('json');
throw $e;
}
if (null === $response) {
return;
}
$event->setResponse($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->migrateSession($request, $token);
$this->tokenStorage->setToken($token);
if (null !== $this->eventDispatcher) {
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
}
if (!$this->successHandler) {
return null; // let the original request succeeds
}
$response = $this->successHandler->onAuthenticationSuccess($request, $token);
if (!$response instanceof Response) {
throw new \RuntimeException('Authentication Success Handler did not return a Response.');
}
return $response;
}
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->getFirewallName()) {
$this->tokenStorage->setToken(null);
}
if (!$this->failureHandler) {
if (null !== $this->translator) {
$errorMessage = $this->translator->trans($failed->getMessageKey(), $failed->getMessageData(), 'security');
} else {
$errorMessage = strtr($failed->getMessageKey(), $failed->getMessageData());
}
return new JsonResponse(['error' => $errorMessage], 401);
}
$response = $this->failureHandler->onAuthenticationFailure($request, $failed);
if (!$response instanceof Response) {
throw new \RuntimeException('Authentication Failure Handler did not return a Response.');
}
return $response;
}
/**
* Call this method if your authentication token is stored to a session.
*
* @final
*/
public function setSessionAuthenticationStrategy(SessionAuthenticationStrategyInterface $sessionStrategy)
{
$this->sessionStrategy = $sessionStrategy;
}
public function setTranslator(TranslatorInterface $translator)
{
$this->translator = $translator;
}
private function migrateSession(Request $request, TokenInterface $token)
{
if (!$this->sessionStrategy || !$request->hasSession() || !$request->hasPreviousSession()) {
return;
}
$this->sessionStrategy->onAuthentication($request, $token);
}
}