vendor/symfony/security-http/Authentication/AuthenticatorManager.php line 228

Open in your IDE?
  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\Authentication;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\HttpFoundation\Request;
  13. use Symfony\Component\HttpFoundation\Response;
  14. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  15. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  16. use Symfony\Component\Security\Core\AuthenticationEvents;
  17. use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
  18. use Symfony\Component\Security\Core\Exception\AccountStatusException;
  19. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  20. use Symfony\Component\Security\Core\Exception\BadCredentialsException;
  21. use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
  22. use Symfony\Component\Security\Core\Exception\UserNotFoundException;
  23. use Symfony\Component\Security\Core\User\UserInterface;
  24. use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
  25. use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticator;
  26. use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
  27. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
  28. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  29. use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
  30. use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
  31. use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
  32. use Symfony\Component\Security\Http\Event\CheckPassportEvent;
  33. use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
  34. use Symfony\Component\Security\Http\Event\LoginFailureEvent;
  35. use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
  36. use Symfony\Component\Security\Http\SecurityEvents;
  37. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  38. /**
  39. * @author Wouter de Jong <wouter@wouterj.nl>
  40. * @author Ryan Weaver <ryan@symfonycasts.com>
  41. * @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
  42. */
  43. class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface
  44. {
  45. private $authenticators;
  46. private $tokenStorage;
  47. private $eventDispatcher;
  48. private $eraseCredentials;
  49. private $logger;
  50. private $firewallName;
  51. private $hideUserNotFoundExceptions;
  52. private $requiredBadges;
  53. /**
  54. * @param iterable<mixed, AuthenticatorInterface> $authenticators
  55. */
  56. public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $firewallName, ?LoggerInterface $logger = null, bool $eraseCredentials = true, bool $hideUserNotFoundExceptions = true, array $requiredBadges = [])
  57. {
  58. $this->authenticators = $authenticators;
  59. $this->tokenStorage = $tokenStorage;
  60. $this->eventDispatcher = $eventDispatcher;
  61. $this->firewallName = $firewallName;
  62. $this->logger = $logger;
  63. $this->eraseCredentials = $eraseCredentials;
  64. $this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions;
  65. $this->requiredBadges = $requiredBadges;
  66. }
  67. /**
  68. * @param BadgeInterface[] $badges Optionally, pass some Passport badges to use for the manual login
  69. */
  70. public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response
  71. {
  72. // create an authentication token for the User
  73. // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0
  74. $passport = new SelfValidatingPassport(new UserBadge(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), function () use ($user) { return $user; }), $badges);
  75. $token = method_exists($authenticator, 'createToken') ? $authenticator->createToken($passport, $this->firewallName) : $authenticator->createAuthenticatedToken($passport, $this->firewallName);
  76. // announce the authentication token
  77. $token = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($token, $passport))->getAuthenticatedToken();
  78. // authenticate this in the system
  79. return $this->handleAuthenticationSuccess($token, $passport, $request, $authenticator, $this->tokenStorage->getToken());
  80. }
  81. public function supports(Request $request): ?bool
  82. {
  83. if (null !== $this->logger) {
  84. $context = ['firewall_name' => $this->firewallName];
  85. if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) {
  86. $context['authenticators'] = \count($this->authenticators);
  87. }
  88. $this->logger->debug('Checking for authenticator support.', $context);
  89. }
  90. $authenticators = [];
  91. $skippedAuthenticators = [];
  92. $lazy = true;
  93. foreach ($this->authenticators as $authenticator) {
  94. if (null !== $this->logger) {
  95. $this->logger->debug('Checking support on authenticator.', ['firewall_name' => $this->firewallName, 'authenticator' => \get_class($authenticator)]);
  96. }
  97. if (false !== $supports = $authenticator->supports($request)) {
  98. $authenticators[] = $authenticator;
  99. $lazy = $lazy && null === $supports;
  100. } else {
  101. if (null !== $this->logger) {
  102. $this->logger->debug('Authenticator does not support the request.', ['firewall_name' => $this->firewallName, 'authenticator' => \get_class($authenticator)]);
  103. }
  104. $skippedAuthenticators[] = $authenticator;
  105. }
  106. }
  107. if (!$authenticators) {
  108. return false;
  109. }
  110. $request->attributes->set('_security_authenticators', $authenticators);
  111. $request->attributes->set('_security_skipped_authenticators', $skippedAuthenticators);
  112. return $lazy ? null : true;
  113. }
  114. public function authenticateRequest(Request $request): ?Response
  115. {
  116. $authenticators = $request->attributes->get('_security_authenticators');
  117. $request->attributes->remove('_security_authenticators');
  118. $request->attributes->remove('_security_skipped_authenticators');
  119. if (!$authenticators) {
  120. return null;
  121. }
  122. return $this->executeAuthenticators($authenticators, $request);
  123. }
  124. /**
  125. * @param AuthenticatorInterface[] $authenticators
  126. */
  127. private function executeAuthenticators(array $authenticators, Request $request): ?Response
  128. {
  129. foreach ($authenticators as $authenticator) {
  130. // recheck if the authenticator still supports the listener. supports() is called
  131. // eagerly (before token storage is initialized), whereas authenticate() is called
  132. // lazily (after initialization).
  133. if (false === $authenticator->supports($request)) {
  134. if (null !== $this->logger) {
  135. $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]);
  136. }
  137. continue;
  138. }
  139. $response = $this->executeAuthenticator($authenticator, $request);
  140. if (null !== $response) {
  141. if (null !== $this->logger) {
  142. $this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]);
  143. }
  144. return $response;
  145. }
  146. }
  147. return null;
  148. }
  149. private function executeAuthenticator(AuthenticatorInterface $authenticator, Request $request): ?Response
  150. {
  151. $passport = null;
  152. $previousToken = $this->tokenStorage->getToken();
  153. try {
  154. // get the passport from the Authenticator
  155. $passport = $authenticator->authenticate($request);
  156. // check the passport (e.g. password checking)
  157. $event = new CheckPassportEvent($authenticator, $passport);
  158. $this->eventDispatcher->dispatch($event);
  159. // check if all badges are resolved
  160. $resolvedBadges = [];
  161. foreach ($passport->getBadges() as $badge) {
  162. if (!$badge->isResolved()) {
  163. throw new BadCredentialsException(sprintf('Authentication failed: Security badge "%s" is not resolved, did you forget to register the correct listeners?', get_debug_type($badge)));
  164. }
  165. $resolvedBadges[] = \get_class($badge);
  166. }
  167. $missingRequiredBadges = array_diff($this->requiredBadges, $resolvedBadges);
  168. if ($missingRequiredBadges) {
  169. throw new BadCredentialsException(sprintf('Authentication failed; Some badges marked as required by the firewall config are not available on the passport: "%s".', implode('", "', $missingRequiredBadges)));
  170. }
  171. // create the authentication token
  172. $authenticatedToken = method_exists($authenticator, 'createToken') ? $authenticator->createToken($passport, $this->firewallName) : $authenticator->createAuthenticatedToken($passport, $this->firewallName);
  173. // announce the authentication token
  174. $authenticatedToken = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($authenticatedToken, $passport))->getAuthenticatedToken();
  175. if (true === $this->eraseCredentials) {
  176. $authenticatedToken->eraseCredentials();
  177. }
  178. $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS);
  179. if (null !== $this->logger) {
  180. $this->logger->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]);
  181. }
  182. } catch (AuthenticationException $e) {
  183. // oh no! Authentication failed!
  184. $response = $this->handleAuthenticationFailure($e, $request, $authenticator, $passport);
  185. if ($response instanceof Response) {
  186. return $response;
  187. }
  188. return null;
  189. }
  190. // success! (sets the token on the token storage, etc)
  191. $response = $this->handleAuthenticationSuccess($authenticatedToken, $passport, $request, $authenticator, $previousToken);
  192. if ($response instanceof Response) {
  193. return $response;
  194. }
  195. if (null !== $this->logger) {
  196. $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]);
  197. }
  198. return null;
  199. }
  200. private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, PassportInterface $passport, Request $request, AuthenticatorInterface $authenticator, ?TokenInterface $previousToken): ?Response
  201. {
  202. // @deprecated since Symfony 5.3
  203. $user = $authenticatedToken->getUser();
  204. if ($user instanceof UserInterface && !method_exists($user, 'getUserIdentifier')) {
  205. trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "getUserIdentifier(): string" in user class "%s" is deprecated. This method will replace "getUsername()" in Symfony 6.0.', get_debug_type($authenticatedToken->getUser()));
  206. }
  207. $this->tokenStorage->setToken($authenticatedToken);
  208. $response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->firewallName);
  209. if ($authenticator instanceof InteractiveAuthenticatorInterface && $authenticator->isInteractive()) {
  210. $loginEvent = new InteractiveLoginEvent($request, $authenticatedToken);
  211. $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
  212. }
  213. $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $passport, $authenticatedToken, $request, $response, $this->firewallName, $previousToken));
  214. return $loginSuccessEvent->getResponse();
  215. }
  216. /**
  217. * Handles an authentication failure and returns the Response for the authenticator.
  218. */
  219. private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator, ?PassportInterface $passport): ?Response
  220. {
  221. if (null !== $this->logger) {
  222. $this->logger->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]);
  223. }
  224. // Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status)
  225. // to prevent user enumeration via response content comparison
  226. if ($this->hideUserNotFoundExceptions && ($authenticationException instanceof UserNotFoundException || ($authenticationException instanceof AccountStatusException && !$authenticationException instanceof CustomUserMessageAccountStatusException))) {
  227. $authenticationException = new BadCredentialsException('Bad credentials.', 0, $authenticationException);
  228. }
  229. $response = $authenticator->onAuthenticationFailure($request, $authenticationException);
  230. if (null !== $response && null !== $this->logger) {
  231. $this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator instanceof TraceableAuthenticator ? $authenticator->getAuthenticator() : $authenticator)]);
  232. }
  233. $this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->firewallName, $passport));
  234. // returning null is ok, it means they want the request to continue
  235. return $loginFailureEvent->getResponse();
  236. }
  237. }