<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Authenticator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
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\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\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Provides a stateless implementation of an authentication via
* a JSON document composed of a username and a password.
*
* @author Kévin Dunglas <[email protected]>
* @author Wouter de Jong <[email protected]>
*
* @final
*/
class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface
{
private array $options;
private $httpUtils;
private $userProvider;
private $propertyAccessor;
private $successHandler;
private $failureHandler;
private $translator = null;
public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, AuthenticationSuccessHandlerInterface $successHandler = null, AuthenticationFailureHandlerInterface $failureHandler = null, array $options = [], PropertyAccessorInterface $propertyAccessor = null)
{
$this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options);
$this->httpUtils = $httpUtils;
$this->successHandler = $successHandler;
$this->failureHandler = $failureHandler;
$this->userProvider = $userProvider;
$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;
}
public function authenticate(Request $request): Passport
{
try {
$credentials = $this->getCredentials($request);
} catch (BadRequestHttpException $e) {
$request->setRequestFormat('json');
throw $e;
}
$passport = new Passport(
new UserBadge($credentials['username'], [$this->userProvider, 'loadUserByIdentifier']),
new PasswordCredentials($credentials['password'])
);
if ($this->userProvider instanceof PasswordUpgraderInterface) {
$passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
}
return $passport;
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if (null === $this->successHandler) {
return null; // let the original request continue
}
return $this->successHandler->onAuthenticationSuccess($request, $token);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if (null === $this->failureHandler) {
if (null !== $this->translator) {
$errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
} else {
$errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
}
return new JsonResponse(['error' => $errorMessage], JsonResponse::HTTP_UNAUTHORIZED);
}
return $this->failureHandler->onAuthenticationFailure($request, $exception);
}
public function isInteractive(): bool
{
return true;
}
public function setTranslator(TranslatorInterface $translator)
{
$this->translator = $translator;
}
private function getCredentials(Request $request)
{
$data = json_decode($request->getContent());
if (!$data instanceof \stdClass) {
throw new BadRequestHttpException('Invalid JSON.');
}
$credentials = [];
try {
$credentials['username'] = $this->propertyAccessor->getValue($data, $this->options['username_path']);
if (!\is_string($credentials['username'])) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['username_path']));
}
if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Invalid username.');
}
} catch (AccessException $e) {
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['username_path']), $e);
}
try {
$credentials['password'] = $this->propertyAccessor->getValue($data, $this->options['password_path']);
if (!\is_string($credentials['password'])) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['password_path']));
}
} catch (AccessException $e) {
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
}
return $credentials;
}
}