AuthenticatorManager.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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\AuthenticationException;
  19. use Symfony\Component\Security\Core\User\UserInterface;
  20. use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
  21. use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
  22. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
  23. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  24. use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
  25. use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
  26. use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
  27. use Symfony\Component\Security\Http\Event\CheckPassportEvent;
  28. use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
  29. use Symfony\Component\Security\Http\Event\LoginFailureEvent;
  30. use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
  31. use Symfony\Component\Security\Http\SecurityEvents;
  32. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  33. /**
  34. * @author Wouter de Jong <wouter@wouterj.nl>
  35. * @author Ryan Weaver <ryan@symfonycasts.com>
  36. * @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
  37. *
  38. * @experimental in 5.2
  39. */
  40. class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface
  41. {
  42. private $authenticators;
  43. private $tokenStorage;
  44. private $eventDispatcher;
  45. private $eraseCredentials;
  46. private $logger;
  47. private $firewallName;
  48. /**
  49. * @param AuthenticatorInterface[] $authenticators
  50. */
  51. public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $firewallName, ?LoggerInterface $logger = null, bool $eraseCredentials = true)
  52. {
  53. $this->authenticators = $authenticators;
  54. $this->tokenStorage = $tokenStorage;
  55. $this->eventDispatcher = $eventDispatcher;
  56. $this->firewallName = $firewallName;
  57. $this->logger = $logger;
  58. $this->eraseCredentials = $eraseCredentials;
  59. }
  60. /**
  61. * @param BadgeInterface[] $badges Optionally, pass some Passport badges to use for the manual login
  62. */
  63. public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response
  64. {
  65. // create an authenticated token for the User
  66. $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport(new UserBadge($user->getUsername(), function () use ($user) { return $user; }), $badges), $this->firewallName);
  67. // announce the authenticated token
  68. $token = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($token))->getAuthenticatedToken();
  69. // authenticate this in the system
  70. return $this->handleAuthenticationSuccess($token, $passport, $request, $authenticator);
  71. }
  72. public function supports(Request $request): ?bool
  73. {
  74. if (null !== $this->logger) {
  75. $context = ['firewall_name' => $this->firewallName];
  76. if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) {
  77. $context['authenticators'] = \count($this->authenticators);
  78. }
  79. $this->logger->debug('Checking for authenticator support.', $context);
  80. }
  81. $authenticators = [];
  82. $lazy = true;
  83. foreach ($this->authenticators as $authenticator) {
  84. if (null !== $this->logger) {
  85. $this->logger->debug('Checking support on authenticator.', ['firewall_name' => $this->firewallName, 'authenticator' => \get_class($authenticator)]);
  86. }
  87. if (false !== $supports = $authenticator->supports($request)) {
  88. $authenticators[] = $authenticator;
  89. $lazy = $lazy && null === $supports;
  90. } elseif (null !== $this->logger) {
  91. $this->logger->debug('Authenticator does not support the request.', ['firewall_name' => $this->firewallName, 'authenticator' => \get_class($authenticator)]);
  92. }
  93. }
  94. if (!$authenticators) {
  95. return false;
  96. }
  97. $request->attributes->set('_security_authenticators', $authenticators);
  98. return $lazy ? null : true;
  99. }
  100. public function authenticateRequest(Request $request): ?Response
  101. {
  102. $authenticators = $request->attributes->get('_security_authenticators');
  103. $request->attributes->remove('_security_authenticators');
  104. if (!$authenticators) {
  105. return null;
  106. }
  107. return $this->executeAuthenticators($authenticators, $request);
  108. }
  109. /**
  110. * @param AuthenticatorInterface[] $authenticators
  111. */
  112. private function executeAuthenticators(array $authenticators, Request $request): ?Response
  113. {
  114. foreach ($authenticators as $authenticator) {
  115. // recheck if the authenticator still supports the listener. supports() is called
  116. // eagerly (before token storage is initialized), whereas authenticate() is called
  117. // lazily (after initialization).
  118. if (false === $authenticator->supports($request)) {
  119. if (null !== $this->logger) {
  120. $this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]);
  121. }
  122. continue;
  123. }
  124. $response = $this->executeAuthenticator($authenticator, $request);
  125. if (null !== $response) {
  126. if (null !== $this->logger) {
  127. $this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator)]);
  128. }
  129. return $response;
  130. }
  131. }
  132. return null;
  133. }
  134. private function executeAuthenticator(AuthenticatorInterface $authenticator, Request $request): ?Response
  135. {
  136. $passport = null;
  137. try {
  138. // get the passport from the Authenticator
  139. $passport = $authenticator->authenticate($request);
  140. // check the passport (e.g. password checking)
  141. $event = new CheckPassportEvent($authenticator, $passport);
  142. $this->eventDispatcher->dispatch($event);
  143. // check if all badges are resolved
  144. $passport->checkIfCompletelyResolved();
  145. // create the authenticated token
  146. $authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->firewallName);
  147. // announce the authenticated token
  148. $authenticatedToken = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($authenticatedToken))->getAuthenticatedToken();
  149. if (true === $this->eraseCredentials) {
  150. $authenticatedToken->eraseCredentials();
  151. }
  152. $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS);
  153. if (null !== $this->logger) {
  154. $this->logger->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => \get_class($authenticator)]);
  155. }
  156. } catch (AuthenticationException $e) {
  157. // oh no! Authentication failed!
  158. $response = $this->handleAuthenticationFailure($e, $request, $authenticator, $passport);
  159. if ($response instanceof Response) {
  160. return $response;
  161. }
  162. return null;
  163. }
  164. // success! (sets the token on the token storage, etc)
  165. $response = $this->handleAuthenticationSuccess($authenticatedToken, $passport, $request, $authenticator);
  166. if ($response instanceof Response) {
  167. return $response;
  168. }
  169. if (null !== $this->logger) {
  170. $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]);
  171. }
  172. return null;
  173. }
  174. private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, PassportInterface $passport, Request $request, AuthenticatorInterface $authenticator): ?Response
  175. {
  176. $this->tokenStorage->setToken($authenticatedToken);
  177. $response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->firewallName);
  178. if ($authenticator instanceof InteractiveAuthenticatorInterface && $authenticator->isInteractive()) {
  179. $loginEvent = new InteractiveLoginEvent($request, $authenticatedToken);
  180. $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
  181. }
  182. $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $passport, $authenticatedToken, $request, $response, $this->firewallName));
  183. return $loginSuccessEvent->getResponse();
  184. }
  185. /**
  186. * Handles an authentication failure and returns the Response for the authenticator.
  187. */
  188. private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator, ?PassportInterface $passport): ?Response
  189. {
  190. if (null !== $this->logger) {
  191. $this->logger->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => \get_class($authenticator)]);
  192. }
  193. $response = $authenticator->onAuthenticationFailure($request, $authenticationException);
  194. if (null !== $response && null !== $this->logger) {
  195. $this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator)]);
  196. }
  197. $this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->firewallName, $passport));
  198. // returning null is ok, it means they want the request to continue
  199. return $loginFailureEvent->getResponse();
  200. }
  201. }