JsonLoginAuthenticator.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  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\Exception\AuthenticationServiceException;
  22. use Symfony\Component\Security\Core\Exception\BadCredentialsException;
  23. use Symfony\Component\Security\Core\Security;
  24. use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
  25. use Symfony\Component\Security\Core\User\UserInterface;
  26. use Symfony\Component\Security\Core\User\UserProviderInterface;
  27. use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
  28. use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
  29. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
  30. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  31. use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
  32. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  33. use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
  34. use Symfony\Component\Security\Http\HttpUtils;
  35. use Symfony\Contracts\Translation\TranslatorInterface;
  36. /**
  37. * Provides a stateless implementation of an authentication via
  38. * a JSON document composed of a username and a password.
  39. *
  40. * @author Kévin Dunglas <dunglas@gmail.com>
  41. * @author Wouter de Jong <wouter@wouterj.nl>
  42. *
  43. * @final
  44. * @experimental in 5.2
  45. */
  46. class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface
  47. {
  48. private $options;
  49. private $httpUtils;
  50. private $userProvider;
  51. private $propertyAccessor;
  52. private $successHandler;
  53. private $failureHandler;
  54. /**
  55. * @var TranslatorInterface|null
  56. */
  57. private $translator;
  58. public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, ?AuthenticationSuccessHandlerInterface $successHandler = null, ?AuthenticationFailureHandlerInterface $failureHandler = null, array $options = [], ?PropertyAccessorInterface $propertyAccessor = null)
  59. {
  60. $this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options);
  61. $this->httpUtils = $httpUtils;
  62. $this->successHandler = $successHandler;
  63. $this->failureHandler = $failureHandler;
  64. $this->userProvider = $userProvider;
  65. $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
  66. }
  67. public function supports(Request $request): ?bool
  68. {
  69. if (false === strpos($request->getRequestFormat(), 'json') && false === strpos($request->getContentType(), 'json')) {
  70. return false;
  71. }
  72. if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
  73. return false;
  74. }
  75. return true;
  76. }
  77. public function authenticate(Request $request): PassportInterface
  78. {
  79. try {
  80. $credentials = $this->getCredentials($request);
  81. } catch (BadRequestHttpException $e) {
  82. $request->setRequestFormat('json');
  83. throw $e;
  84. }
  85. $passport = new Passport(new UserBadge($credentials['username'], function ($username) {
  86. $user = $this->userProvider->loadUserByUsername($username);
  87. if (!$user instanceof UserInterface) {
  88. throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
  89. }
  90. return $user;
  91. }), new PasswordCredentials($credentials['password']));
  92. if ($this->userProvider instanceof PasswordUpgraderInterface) {
  93. $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
  94. }
  95. return $passport;
  96. }
  97. public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
  98. {
  99. return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
  100. }
  101. public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
  102. {
  103. if (null === $this->successHandler) {
  104. return null; // let the original request continue
  105. }
  106. return $this->successHandler->onAuthenticationSuccess($request, $token);
  107. }
  108. public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
  109. {
  110. if (null === $this->failureHandler) {
  111. if (null !== $this->translator) {
  112. $errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
  113. } else {
  114. $errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
  115. }
  116. return new JsonResponse(['error' => $errorMessage], JsonResponse::HTTP_UNAUTHORIZED);
  117. }
  118. return $this->failureHandler->onAuthenticationFailure($request, $exception);
  119. }
  120. public function isInteractive(): bool
  121. {
  122. return true;
  123. }
  124. public function setTranslator(TranslatorInterface $translator)
  125. {
  126. $this->translator = $translator;
  127. }
  128. private function getCredentials(Request $request)
  129. {
  130. $data = json_decode($request->getContent());
  131. if (!$data instanceof \stdClass) {
  132. throw new BadRequestHttpException('Invalid JSON.');
  133. }
  134. $credentials = [];
  135. try {
  136. $credentials['username'] = $this->propertyAccessor->getValue($data, $this->options['username_path']);
  137. if (!\is_string($credentials['username'])) {
  138. throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['username_path']));
  139. }
  140. if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) {
  141. throw new BadCredentialsException('Invalid username.');
  142. }
  143. } catch (AccessException $e) {
  144. throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['username_path']), $e);
  145. }
  146. try {
  147. $credentials['password'] = $this->propertyAccessor->getValue($data, $this->options['password_path']);
  148. if (!\is_string($credentials['password'])) {
  149. throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['password_path']));
  150. }
  151. } catch (AccessException $e) {
  152. throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
  153. }
  154. return $credentials;
  155. }
  156. }