vendor/scheb/2fa-bundle/Security/Http/Authenticator/TwoFactorAuthenticator.php line 36

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Scheb\TwoFactorBundle\Security\Http\Authenticator;
  4. use Psr\Log\LoggerInterface;
  5. use Psr\Log\NullLogger;
  6. use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
  7. use Scheb\TwoFactorBundle\Security\Http\Authentication\AuthenticationRequiredHandlerInterface;
  8. use Scheb\TwoFactorBundle\Security\Http\Authenticator\Passport\Badge\TrustedDeviceBadge;
  9. use Scheb\TwoFactorBundle\Security\Http\Authenticator\Passport\Credentials\TwoFactorCodeCredentials;
  10. use Scheb\TwoFactorBundle\Security\Http\Authenticator\Passport\TwoFactorPassport;
  11. use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvent;
  12. use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvents;
  13. use Scheb\TwoFactorBundle\Security\TwoFactor\TwoFactorFirewallConfig;
  14. use Scheb\TwoFactorBundle\Security\UsernameHelper;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  18. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  19. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  20. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  21. use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
  22. use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
  23. use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
  24. use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
  25. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
  26. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
  27. use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
  28. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  29. /**
  30.  * @final
  31.  */
  32. class TwoFactorAuthenticator implements AuthenticatorInterfaceInteractiveAuthenticatorInterface
  33. {
  34.     public const FLAG_2FA_COMPLETE '2fa_complete';
  35.     /**
  36.      * @var TwoFactorFirewallConfig
  37.      */
  38.     private $twoFactorFirewallConfig;
  39.     /**
  40.      * @var TokenStorageInterface
  41.      */
  42.     private $tokenStorage;
  43.     /**
  44.      * @var AuthenticationSuccessHandlerInterface
  45.      */
  46.     private $successHandler;
  47.     /**
  48.      * @var AuthenticationFailureHandlerInterface
  49.      */
  50.     private $failureHandler;
  51.     /**
  52.      * @var AuthenticationRequiredHandlerInterface
  53.      */
  54.     private $authenticationRequiredHandler;
  55.     /**
  56.      * @var EventDispatcherInterface
  57.      */
  58.     private $eventDispatcher;
  59.     /**
  60.      * @var LoggerInterface
  61.      */
  62.     private $logger;
  63.     public function __construct(
  64.         TwoFactorFirewallConfig $twoFactorFirewallConfig,
  65.         TokenStorageInterface $tokenStorage,
  66.         AuthenticationSuccessHandlerInterface $successHandler,
  67.         AuthenticationFailureHandlerInterface $failureHandler,
  68.         AuthenticationRequiredHandlerInterface $authenticationRequiredHandler,
  69.         EventDispatcherInterface $eventDispatcher,
  70.         ?LoggerInterface $logger null
  71.     ) {
  72.         $this->twoFactorFirewallConfig $twoFactorFirewallConfig;
  73.         $this->tokenStorage $tokenStorage;
  74.         $this->successHandler $successHandler;
  75.         $this->failureHandler $failureHandler;
  76.         $this->authenticationRequiredHandler $authenticationRequiredHandler;
  77.         $this->eventDispatcher $eventDispatcher;
  78.         $this->logger $logger ?? new NullLogger();
  79.     }
  80.     public function supports(Request $request): ?bool
  81.     {
  82.         return $this->twoFactorFirewallConfig->isCheckPathRequest($request);
  83.     }
  84.     /**
  85.      * @psalm-suppress InvalidReturnType
  86.      */
  87.     public function authenticate(Request $request): PassportInterface
  88.     {
  89.         // When the firewall is lazy, the token is not initialized in the "supports" stage, so this check does only work
  90.         // within the "authenticate" stage.
  91.         $currentToken $this->tokenStorage->getToken();
  92.         if (!($currentToken instanceof TwoFactorTokenInterface)) {
  93.             // This should only happen when the check path is called outside of a 2fa process
  94.             // access_control can't handle this, as it's called after the authenticator
  95.             throw new AccessDeniedException('User is not in a two-factor authentication process.');
  96.         }
  97.         $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::ATTEMPT$request$currentToken);
  98.         $credentials = new TwoFactorCodeCredentials($this->twoFactorFirewallConfig->getAuthCodeFromRequest($request));
  99.         $passport = new TwoFactorPassport($currentToken$credentials, []);
  100.         if ($currentToken->hasAttribute(TwoFactorTokenInterface::ATTRIBUTE_NAME_USE_REMEMBER_ME)) {
  101.             $rememberMeBadge = new RememberMeBadge();
  102.             $rememberMeBadge->enable();
  103.             $passport->addBadge($rememberMeBadge);
  104.         }
  105.         if ($this->twoFactorFirewallConfig->isCsrfProtectionEnabled()) {
  106.             $tokenValue $this->twoFactorFirewallConfig->getCsrfTokenFromRequest($request);
  107.             $tokenId $this->twoFactorFirewallConfig->getCsrfTokenId();
  108.             $passport->addBadge(new CsrfTokenBadge($tokenId$tokenValue));
  109.         }
  110.         // Make sure the trusted device package is installed
  111.         if (class_exists(TrustedDeviceBadge::class) && $this->shouldSetTrustedDevice($request$passport)) {
  112.             $passport->addBadge(new TrustedDeviceBadge());
  113.         }
  114.         /** @psalm-suppress InvalidReturnStatement */
  115.         return $passport;
  116.     }
  117.     private function shouldSetTrustedDevice(Request $requestTwoFactorPassport $passport): bool
  118.     {
  119.         return $this->twoFactorFirewallConfig->hasTrustedDeviceParameterInRequest($request)
  120.             || (
  121.                 $this->twoFactorFirewallConfig->isRememberMeSetsTrusted()
  122.                 && $passport->hasBadge(RememberMeBadge::class)
  123.             );
  124.     }
  125.     public function createAuthenticatedToken(PassportInterface $passportstring $firewallName): TokenInterface
  126.     {
  127.         /** @var TwoFactorPassport $passport */
  128.         $twoFactorToken $passport->getTwoFactorToken();
  129.         if ($this->isAuthenticationComplete($twoFactorToken)) {
  130.             $authenticatedToken $twoFactorToken->getAuthenticatedToken(); // Authentication complete, unwrap the token
  131.             $authenticatedToken->setAttribute(self::FLAG_2FA_COMPLETEtrue);
  132.             return $authenticatedToken;
  133.         }
  134.         return $twoFactorToken;
  135.     }
  136.     private function isAuthenticationComplete(TwoFactorTokenInterface $token): bool
  137.     {
  138.         return !$this->twoFactorFirewallConfig->isMultiFactor() || $token->allTwoFactorProvidersAuthenticated();
  139.     }
  140.     public function onAuthenticationSuccess(Request $requestTokenInterface $tokenstring $firewallName): ?Response
  141.     {
  142.         $this->logger->info('User has been two-factor authenticated successfully.', ['username' => UsernameHelper::getTokenUsername($token)]);
  143.         $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::SUCCESS$request$token);
  144.         // When it's still a TwoFactorTokenInterface, keep showing the auth form
  145.         if ($token instanceof TwoFactorTokenInterface) {
  146.             $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::REQUIRE, $request$token);
  147.             return $this->authenticationRequiredHandler->onAuthenticationRequired($request$token);
  148.         }
  149.         $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::COMPLETE$request$token);
  150.         return $this->successHandler->onAuthenticationSuccess($request$token);
  151.     }
  152.     public function onAuthenticationFailure(Request $requestAuthenticationException $exception): ?Response
  153.     {
  154.         /** @var TwoFactorTokenInterface $currentToken */
  155.         $currentToken $this->tokenStorage->getToken();
  156.         $this->logger->info('Two-factor authentication request failed.', ['exception' => $exception]);
  157.         $this->dispatchTwoFactorAuthenticationEvent(TwoFactorAuthenticationEvents::FAILURE$request$currentToken);
  158.         return $this->failureHandler->onAuthenticationFailure($request$exception);
  159.     }
  160.     private function dispatchTwoFactorAuthenticationEvent(string $eventTypeRequest $requestTokenInterface $token): void
  161.     {
  162.         $event = new TwoFactorAuthenticationEvent($request$token);
  163.         $this->eventDispatcher->dispatch($event$eventType);
  164.     }
  165.     public function isInteractive(): bool
  166.     {
  167.         return true;
  168.     }
  169. }