vendor/symfony/security-http/Authenticator/JsonLoginAuthenticator.php line 82

  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <[email protected]>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Security\Http\Authenticator;
  11. use Symfony\Component\HttpFoundation\JsonResponse;
  12. use Symfony\Component\HttpFoundation\Request;
  13. use Symfony\Component\HttpFoundation\Response;
  14. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  15. use Symfony\Component\PropertyAccess\Exception\AccessException;
  16. use Symfony\Component\PropertyAccess\PropertyAccess;
  17. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  18. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  19. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  20. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  21. use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
  22. use Symfony\Component\Security\Core\User\UserProviderInterface;
  23. use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
  24. use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
  25. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
  26. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
  27. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  28. use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
  29. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  30. use Symfony\Component\Security\Http\HttpUtils;
  31. use Symfony\Contracts\Translation\TranslatorInterface;
  32. /**
  33. * Provides a stateless implementation of an authentication via
  34. * a JSON document composed of a username and a password.
  35. *
  36. * @author Kévin Dunglas <[email protected]>
  37. * @author Wouter de Jong <[email protected]>
  38. *
  39. * @final
  40. */
  41. class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface
  42. {
  43. private array $options;
  44. private PropertyAccessorInterface $propertyAccessor;
  45. private ?TranslatorInterface $translator = null;
  46. public function __construct(
  47. private HttpUtils $httpUtils,
  48. private UserProviderInterface $userProvider,
  49. private ?AuthenticationSuccessHandlerInterface $successHandler = null,
  50. private ?AuthenticationFailureHandlerInterface $failureHandler = null,
  51. array $options = [],
  52. ?PropertyAccessorInterface $propertyAccessor = null,
  53. ) {
  54. $this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options);
  55. $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
  56. }
  57. public function supports(Request $request): ?bool
  58. {
  59. if (
  60. !str_contains($request->getRequestFormat() ?? '', 'json')
  61. && !str_contains($request->getContentTypeFormat() ?? '', 'json')
  62. ) {
  63. return false;
  64. }
  65. if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
  66. return false;
  67. }
  68. return true;
  69. }
  70. public function authenticate(Request $request): Passport
  71. {
  72. try {
  73. $data = json_decode($request->getContent());
  74. if (!$data instanceof \stdClass) {
  75. throw new BadRequestHttpException('Invalid JSON.');
  76. }
  77. $credentials = $this->getCredentials($data);
  78. } catch (BadRequestHttpException $e) {
  79. $request->setRequestFormat('json');
  80. throw $e;
  81. }
  82. $userBadge = new UserBadge($credentials['username'], $this->userProvider->loadUserByIdentifier(...));
  83. $passport = new Passport($userBadge, new PasswordCredentials($credentials['password']), [new RememberMeBadge((array) $data)]);
  84. if ($this->userProvider instanceof PasswordUpgraderInterface) {
  85. $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
  86. }
  87. return $passport;
  88. }
  89. public function createToken(Passport $passport, string $firewallName): TokenInterface
  90. {
  91. return new UsernamePasswordToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
  92. }
  93. public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
  94. {
  95. if (null === $this->successHandler) {
  96. return null; // let the original request continue
  97. }
  98. return $this->successHandler->onAuthenticationSuccess($request, $token);
  99. }
  100. public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
  101. {
  102. if (null === $this->failureHandler) {
  103. if (null !== $this->translator) {
  104. $errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
  105. } else {
  106. $errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
  107. }
  108. return new JsonResponse(['error' => $errorMessage], JsonResponse::HTTP_UNAUTHORIZED);
  109. }
  110. return $this->failureHandler->onAuthenticationFailure($request, $exception);
  111. }
  112. public function isInteractive(): bool
  113. {
  114. return true;
  115. }
  116. public function setTranslator(TranslatorInterface $translator): void
  117. {
  118. $this->translator = $translator;
  119. }
  120. private function getCredentials(\stdClass $data): array
  121. {
  122. $credentials = [];
  123. try {
  124. $credentials['username'] = $this->propertyAccessor->getValue($data, $this->options['username_path']);
  125. if (!\is_string($credentials['username']) || '' === $credentials['username']) {
  126. throw new BadRequestHttpException(\sprintf('The key "%s" must be a non-empty string.', $this->options['username_path']));
  127. }
  128. } catch (AccessException $e) {
  129. throw new BadRequestHttpException(\sprintf('The key "%s" must be provided.', $this->options['username_path']), $e);
  130. }
  131. try {
  132. $credentials['password'] = $this->propertyAccessor->getValue($data, $this->options['password_path']);
  133. $this->propertyAccessor->setValue($data, $this->options['password_path'], null);
  134. if (!\is_string($credentials['password']) || '' === $credentials['password']) {
  135. throw new BadRequestHttpException(\sprintf('The key "%s" must be a non-empty string.', $this->options['password_path']));
  136. }
  137. } catch (AccessException $e) {
  138. throw new BadRequestHttpException(\sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
  139. }
  140. return $credentials;
  141. }
  142. }