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

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 AuthenticatorManagerInterfaceUserAuthenticatorInterface
  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 $authenticatorsTokenStorageInterface $tokenStorageEventDispatcherInterface $eventDispatcherstring $firewallName, ?LoggerInterface $logger nullbool $eraseCredentials truebool $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 $userAuthenticatorInterface $authenticatorRequest $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 $authenticatorsRequest $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 $authenticatorRequest $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 $authenticatedTokenPassportInterface $passportRequest $requestAuthenticatorInterface $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($loginEventSecurityEvents::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 $authenticationExceptionRequest $requestAuthenticatorInterface $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. }