vendor/contao/core-bundle/src/Security/Authentication/Provider/AuthenticationProvider.php line 87

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of Contao.
  5.  *
  6.  * (c) Leo Feyer
  7.  *
  8.  * @license LGPL-3.0-or-later
  9.  */
  10. namespace Contao\CoreBundle\Security\Authentication\Provider;
  11. use Contao\CoreBundle\Framework\ContaoFramework;
  12. use Contao\CoreBundle\Security\Exception\LockedException;
  13. use Contao\System;
  14. use Contao\User;
  15. use Scheb\TwoFactorBundle\Security\Authentication\Exception\InvalidTwoFactorCodeException;
  16. use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
  17. use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextFactoryInterface;
  18. use Scheb\TwoFactorBundle\Security\TwoFactor\Handler\AuthenticationHandlerInterface;
  19. use Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\TrustedDeviceManagerInterface;
  20. use Symfony\Component\HttpFoundation\RequestStack;
  21. use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
  22. use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
  23. use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
  24. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  25. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  26. use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
  27. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  28. use Symfony\Component\Security\Core\Exception\BadCredentialsException;
  29. use Symfony\Component\Security\Core\User\UserCheckerInterface;
  30. use Symfony\Component\Security\Core\User\UserInterface;
  31. use Symfony\Component\Security\Core\User\UserProviderInterface;
  32. class AuthenticationProvider extends DaoAuthenticationProvider
  33. {
  34.     /**
  35.      * @var UserCheckerInterface
  36.      */
  37.     private $userChecker;
  38.     /**
  39.      * @var string
  40.      */
  41.     private $providerKey;
  42.     /**
  43.      * @var ContaoFramework
  44.      */
  45.     private $framework;
  46.     /**
  47.      * @var AuthenticationProviderInterface
  48.      */
  49.     private $twoFactorAuthenticationProvider;
  50.     /**
  51.      * @var AuthenticationHandlerInterface
  52.      */
  53.     private $twoFactorAuthenticationHandler;
  54.     /**
  55.      * @var AuthenticationContextFactoryInterface
  56.      */
  57.     private $authenticationContextFactory;
  58.     /**
  59.      * @var RequestStack
  60.      */
  61.     private $requestStack;
  62.     /**
  63.      * @var TrustedDeviceManagerInterface
  64.      */
  65.     private $trustedDeviceManager;
  66.     /**
  67.      * @internal Do not inherit from this class; decorate the "contao.security.authentication_provider" service instead
  68.      *
  69.      * @todo Replace EncoderFactoryInterface with Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface
  70.      */
  71.     public function __construct(UserProviderInterface $userProviderUserCheckerInterface $userCheckerstring $providerKeyEncoderFactoryInterface $encoderFactoryContaoFramework $frameworkAuthenticationProviderInterface $twoFactorAuthenticationProviderAuthenticationHandlerInterface $twoFactorAuthenticationHandlerAuthenticationContextFactoryInterface $authenticationContextFactoryRequestStack $requestStackTrustedDeviceManagerInterface $trustedDeviceManager)
  72.     {
  73.         /** @phpstan-ignore-next-line */
  74.         parent::__construct($userProvider$userChecker$providerKey$encoderFactoryfalse);
  75.         $this->userChecker $userChecker;
  76.         $this->providerKey $providerKey;
  77.         $this->framework $framework;
  78.         $this->twoFactorAuthenticationProvider $twoFactorAuthenticationProvider;
  79.         $this->twoFactorAuthenticationHandler $twoFactorAuthenticationHandler;
  80.         $this->authenticationContextFactory $authenticationContextFactory;
  81.         $this->requestStack $requestStack;
  82.         $this->trustedDeviceManager $trustedDeviceManager;
  83.     }
  84.     public function authenticate(TokenInterface $token): TokenInterface
  85.     {
  86.         if ($token instanceof TwoFactorTokenInterface) {
  87.             return $this->checkTwoFactor($token);
  88.         }
  89.         $wasAlreadyAuthenticated $token->isAuthenticated();
  90.         $token parent::authenticate($token);
  91.         // Only trigger two-factor authentication when the provider was called
  92.         // with an unauthenticated token. When we get an authenticated token,
  93.         // the system will refresh it and starting two-factor authentication
  94.         // would trigger an endless loop.
  95.         if ($wasAlreadyAuthenticated) {
  96.             return $token;
  97.         }
  98.         // AnonymousToken and TwoFactorTokenInterface can be ignored.
  99.         if ($token instanceof AnonymousToken || $token instanceof TwoFactorTokenInterface) {
  100.             return $token;
  101.         }
  102.         // Skip two-factor authentication on trusted devices
  103.         if ($this->trustedDeviceManager->isTrustedDevice($token->getUser(), $this->providerKey)) {
  104.             return $token;
  105.         }
  106.         $request $this->requestStack->getMasterRequest();
  107.         $context $this->authenticationContextFactory->create($request$token$this->providerKey);
  108.         return $this->twoFactorAuthenticationHandler->beginTwoFactorAuthentication($context);
  109.     }
  110.     public function supports(TokenInterface $token): bool
  111.     {
  112.         return parent::supports($token) || $this->twoFactorAuthenticationProvider->supports($token);
  113.     }
  114.     public function checkAuthentication(UserInterface $userUsernamePasswordToken $token): void
  115.     {
  116.         if (!$user instanceof User) {
  117.             parent::checkAuthentication($user$token);
  118.             return;
  119.         }
  120.         try {
  121.             parent::checkAuthentication($user$token);
  122.         } catch (AuthenticationException $exception) {
  123.             if (!$exception instanceof BadCredentialsException) {
  124.                 throw $exception;
  125.             }
  126.             if (!$this->triggerCheckCredentialsHook($user$token)) {
  127.                 $exception = new BadCredentialsException(
  128.                     sprintf('Invalid password submitted for username "%s"'$user->username),
  129.                     $exception->getCode(),
  130.                     $exception
  131.                 );
  132.                 throw $this->onBadCredentials($user$exception);
  133.             }
  134.         }
  135.     }
  136.     private function checkTwoFactor(TokenInterface $token): TokenInterface
  137.     {
  138.         $user $token->getUser();
  139.         if (!$user instanceof User) {
  140.             return $this->twoFactorAuthenticationProvider->authenticate($token);
  141.         }
  142.         try {
  143.             $this->userChecker->checkPreAuth($user);
  144.             $token $this->twoFactorAuthenticationProvider->authenticate($token);
  145.             $this->userChecker->checkPostAuth($user);
  146.             return $token;
  147.         } catch (AuthenticationException $exception) {
  148.             if (!$exception instanceof InvalidTwoFactorCodeException) {
  149.                 throw $exception;
  150.             }
  151.             $exception = new InvalidTwoFactorCodeException(
  152.                 sprintf('Invalid two-factor code submitted for username "%s"'$user->username),
  153.                 $exception->getCode(),
  154.                 $exception
  155.             );
  156.             throw $this->onBadCredentials($user$exception);
  157.         }
  158.     }
  159.     /**
  160.      * Counts the login attempts and locks the user after three failed attempts
  161.      * following a specific delay scheme.
  162.      *
  163.      * After the third failed attempt A, the authentication server waits for an
  164.      * increased (A - 2) * 60 seconds. After 3 attempts, the server waits for 60 seconds,
  165.      * at the fourth failed attempt, it waits for 2 * 60 = 120 seconds and so on.
  166.      */
  167.     private function onBadCredentials(User $userAuthenticationException $exception): AuthenticationException
  168.     {
  169.         ++$user->loginAttempts;
  170.         if ($user->loginAttempts 3) {
  171.             $user->save();
  172.             return $exception;
  173.         }
  174.         $lockedSeconds = ($user->loginAttempts 2) * 60;
  175.         $user->locked time() + $lockedSeconds;
  176.         $user->save();
  177.         $exception = new LockedException(
  178.             $lockedSeconds,
  179.             sprintf('User "%s" has been locked for %s seconds'$user->username$lockedSeconds),
  180.             0,
  181.             $exception
  182.         );
  183.         $exception->setUser($user);
  184.         return $exception;
  185.     }
  186.     private function triggerCheckCredentialsHook(User $userUsernamePasswordToken $token): bool
  187.     {
  188.         $this->framework->initialize();
  189.         if (empty($GLOBALS['TL_HOOKS']['checkCredentials']) || !\is_array($GLOBALS['TL_HOOKS']['checkCredentials'])) {
  190.             return false;
  191.         }
  192.         trigger_deprecation('contao/core-bundle''4.5''Using the "checkCredentials" hook has been deprecated and will no longer work in Contao 5.0.');
  193.         /** @var System $system */
  194.         $system $this->framework->getAdapter(System::class);
  195.         $username $token->getUsername();
  196.         $credentials $token->getCredentials();
  197.         foreach ($GLOBALS['TL_HOOKS']['checkCredentials'] as $callback) {
  198.             if ($system->importStatic($callback[0])->{$callback[1]}($username$credentials$user)) {
  199.                 return true;
  200.             }
  201.         }
  202.         return false;
  203.     }
  204. }