vendor/scheb/2fa-bundle/Security/Http/Firewall/TwoFactorListener.php line 36

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Scheb\TwoFactorBundle\Security\Http\Firewall;
  4. use Psr\Log\LoggerInterface;
  5. use Psr\Log\NullLogger;
  6. use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenFactoryInterface;
  7. use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
  8. use Scheb\TwoFactorBundle\Security\Http\Authentication\AuthenticationRequiredHandlerInterface;
  9. use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvent;
  10. use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvents;
  11. use Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\TrustedDeviceManagerInterface;
  12. use Scheb\TwoFactorBundle\Security\TwoFactor\TwoFactorFirewallConfig;
  13. use Scheb\TwoFactorBundle\Security\UsernameHelper;
  14. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\HttpKernel\Event\RequestEvent;
  18. use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
  19. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  20. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  21. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  22. use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
  23. use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
  24. use Symfony\Component\Security\Csrf\CsrfToken;
  25. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  26. use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
  27. use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
  28. use Symfony\Component\Security\Http\Firewall\AbstractListener;
  29. /**
  30.  * @final
  31.  */
  32. class TwoFactorListener extends AbstractListener
  33. {
  34.     /**
  35.      * @var TokenStorageInterface
  36.      */
  37.     private $tokenStorage;
  38.     /**
  39.      * @var AuthenticationManagerInterface
  40.      */
  41.     private $authenticationManager;
  42.     /**
  43.      * @var TwoFactorFirewallConfig
  44.      */
  45.     private $twoFactorFirewallConfig;
  46.     /**
  47.      * @var AuthenticationSuccessHandlerInterface
  48.      */
  49.     private $successHandler;
  50.     /**
  51.      * @var AuthenticationFailureHandlerInterface
  52.      */
  53.     private $failureHandler;
  54.     /**
  55.      * @var AuthenticationRequiredHandlerInterface
  56.      */
  57.     private $authenticationRequiredHandler;
  58.     /**
  59.      * @var CsrfTokenManagerInterface
  60.      */
  61.     private $csrfTokenManager;
  62.     /**
  63.      * @var TrustedDeviceManagerInterface|null
  64.      */
  65.     private $trustedDeviceManager;
  66.     /**
  67.      * @var EventDispatcherInterface
  68.      */
  69.     private $eventDispatcher;
  70.     /**
  71.      * @var TwoFactorTokenFactoryInterface
  72.      */
  73.     private $twoFactorTokenFactory;
  74.     /**
  75.      * @var LoggerInterface
  76.      */
  77.     private $logger;
  78.     public function __construct(
  79.         TokenStorageInterface $tokenStorage,
  80.         AuthenticationManagerInterface $authenticationManager,
  81.         TwoFactorFirewallConfig $twoFactorFirewallConfig,
  82.         AuthenticationSuccessHandlerInterface $successHandler,
  83.         AuthenticationFailureHandlerInterface $failureHandler,
  84.         AuthenticationRequiredHandlerInterface $authenticationRequiredHandler,
  85.         CsrfTokenManagerInterface $csrfTokenManager,
  86.         ?TrustedDeviceManagerInterface $trustedDeviceManager,
  87.         EventDispatcherInterface $eventDispatcher,
  88.         TwoFactorTokenFactoryInterface $twoFactorTokenFactory,
  89.         ?LoggerInterface $logger null
  90.     ) {
  91.         $this->tokenStorage $tokenStorage;
  92.         $this->authenticationManager $authenticationManager;
  93.         $this->twoFactorFirewallConfig $twoFactorFirewallConfig;
  94.         $this->successHandler $successHandler;
  95.         $this->failureHandler $failureHandler;
  96.         $this->authenticationRequiredHandler $authenticationRequiredHandler;
  97.         $this->csrfTokenManager $csrfTokenManager;
  98.         $this->trustedDeviceManager $trustedDeviceManager;
  99.         $this->eventDispatcher $eventDispatcher;
  100.         $this->twoFactorTokenFactory $twoFactorTokenFactory;
  101.         $this->logger $logger ?? new NullLogger();
  102.     }
  103.     public function supports(Request $request): ?bool
  104.     {
  105.         return $this->twoFactorFirewallConfig->isCheckPathRequest($request);
  106.     }
  107.     public function authenticate(RequestEvent $event): void
  108.     {
  109.         // When the firewall is lazy, the token is not initialized in the "supports" stage, so this check does only work
  110.         // within the "authenticate" stage.
  111.         $token $this->tokenStorage->getToken();
  112.         if (!($token instanceof TwoFactorTokenInterface) || $token->getProviderKey(true) !== $this->twoFactorFirewallConfig->getFirewallName()) {
  113.             // This should only happen when the check path is called outside of a 2fa process and not protected via access_control
  114.             // or when the firewall is configured in an odd way (different firewall name)
  115.             throw new AuthenticationServiceException('Tried to perform two-factor authentication, but two-factor authentication is not in progress.');
  116.         }
  117.         $response $this->attemptAuthentication($event->getRequest(), $token);
  118.         $event->setResponse($response);
  119.     }
  120.     private function attemptAuthentication(Request $requestTwoFactorTokenInterface $beginToken): Response
  121.     {
  122.         $authCode $this->twoFactorFirewallConfig->getAuthCodeFromRequest($request);
  123.         try {
  124.             if (!$this->hasValidCsrfToken($request)) {
  125.                 throw new InvalidCsrfTokenException('Invalid CSRF token.');
  126.             }
  127.             $token $beginToken->createWithCredentials($authCode);
  128.             $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::ATTEMPT$request$token);
  129.             /** @psalm-suppress InternalMethod */
  130.             $resultToken $this->authenticationManager->authenticate($token);
  131.             return $this->onSuccess($request$resultToken$beginToken);
  132.         } catch (AuthenticationException $failureException) {
  133.             return $this->onFailure($request$beginToken$failureException);
  134.         }
  135.     }
  136.     public function hasValidCsrfToken(Request $request): bool
  137.     {
  138.         $tokenValue $this->twoFactorFirewallConfig->getCsrfTokenFromRequest($request);
  139.         $tokenId $this->twoFactorFirewallConfig->getCsrfTokenId();
  140.         $token = new CsrfToken($tokenId$tokenValue);
  141.         return $this->csrfTokenManager->isTokenValid($token);
  142.     }
  143.     private function onFailure(Request $requestTwoFactorTokenInterface $tokenAuthenticationException $failureException): Response
  144.     {
  145.         $this->logger->info('Two-factor authentication request failed.', ['exception' => $failureException]);
  146.         $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::FAILURE$request$token);
  147.         return $this->failureHandler->onAuthenticationFailure($request$failureException);
  148.     }
  149.     private function onSuccess(Request $requestTokenInterface $tokenTwoFactorTokenInterface $previousTwoFactorToken): Response
  150.     {
  151.         $this->logger->info('User has been two-factor authenticated successfully.', ['username' => UsernameHelper::getTokenUsername($token)]);
  152.         $this->tokenStorage->setToken($token);
  153.         $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::SUCCESS$request$token);
  154.         // When it's still a TwoFactorTokenInterface, keep showing the auth form
  155.         if ($token instanceof TwoFactorTokenInterface) {
  156.             $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::REQUIRE, $request$token);
  157.             return $this->authenticationRequiredHandler->onAuthenticationRequired($request$token);
  158.         }
  159.         $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::COMPLETE$request$token);
  160.         $firewallName $previousTwoFactorToken->getProviderKey(true);
  161.         if ($this->trustedDeviceManager // Make sure the trusted device package is installed
  162.             && $this->shouldSetTrustedDevice($request$previousTwoFactorToken)
  163.             && $this->trustedDeviceManager->canSetTrustedDevice($token->getUser(), $request$firewallName)
  164.         ) {
  165.             $this->trustedDeviceManager->addTrustedDevice($token->getUser(), $firewallName);
  166.         }
  167.         $response $this->successHandler->onAuthenticationSuccess($request$token);
  168.         $this->addRememberMeCookies($previousTwoFactorToken$response);
  169.         return $response;
  170.     }
  171.     private function shouldSetTrustedDevice(Request $requestTwoFactorTokenInterface $previousTwoFactorToken): bool
  172.     {
  173.         return $this->twoFactorFirewallConfig->hasTrustedDeviceParameterInRequest($request)
  174.             || (
  175.                 $this->twoFactorFirewallConfig->isRememberMeSetsTrusted()
  176.                 && $previousTwoFactorToken->hasAttribute(TwoFactorTokenInterface::ATTRIBUTE_NAME_REMEMBER_ME_COOKIE)
  177.             );
  178.     }
  179.     private function dispatchTwoFactorAuthenticationEvent(string $eventTypeRequest $requestTokenInterface $token): void
  180.     {
  181.         $event = new TwoFactorAuthenticationEvent($request$token);
  182.         $this->eventDispatcher->dispatch($event$eventType);
  183.     }
  184.     private function addRememberMeCookies(TwoFactorTokenInterface $twoFactorTokenResponse $response): void
  185.     {
  186.         // Add the remember-me cookie that was previously suppressed by two-factor authentication
  187.         if ($twoFactorToken->hasAttribute(TwoFactorTokenInterface::ATTRIBUTE_NAME_REMEMBER_ME_COOKIE)) {
  188.             $rememberMeCookies $twoFactorToken->getAttribute(TwoFactorTokenInterface::ATTRIBUTE_NAME_REMEMBER_ME_COOKIE);
  189.             foreach ($rememberMeCookies as $cookie) {
  190.                 $response->headers->setCookie($cookie);
  191.             }
  192.         }
  193.     }
  194. }