src/Security/Traits/LoginAuthenticatorTrait.php line 217

Open in your IDE?
  1. <?php
  2. namespace App\Security\Traits;
  3. use App\Constants\Platform;
  4. use App\Constants\Setting;
  5. use App\Constants\Modules;
  6. use App\Entity\User;
  7. use App\Services\Common\ModuleSettingService;
  8. use App\Services\Common\SettingService;
  9. use App\Services\ConfigService;
  10. use App\Services\DTV\YamlConfig\YamlReader;
  11. use App\Services\Portal\PortalService;
  12. use DateTime;
  13. use Doctrine\ORM\EntityManagerInterface;
  14. use Exception;
  15. use KnpU\OAuth2ClientBundle\Security\Exception\IdentityProviderAuthenticationException;
  16. use KnpU\OAuth2ClientBundle\Security\Exception\InvalidStateAuthenticationException;
  17. use KnpU\OAuth2ClientBundle\Security\Exception\NoAuthCodeAuthenticationException;
  18. use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
  19. use Psr\Container\ContainerExceptionInterface;
  20. use Psr\Container\NotFoundExceptionInterface;
  21. use Psr\Log\LoggerInterface;
  22. use Symfony\Component\DependencyInjection\ServiceLocator;
  23. use Symfony\Component\HttpFoundation\RedirectResponse;
  24. use Symfony\Component\HttpFoundation\Request;
  25. use Symfony\Component\HttpFoundation\Response;
  26. use Symfony\Component\HttpKernel\KernelInterface;
  27. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  28. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  29. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  30. use Symfony\Component\Security\Core\Exception\BadCredentialsException;
  31. use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  32. use Symfony\Component\Security\Core\Security;
  33. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
  34. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  35. use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
  36. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  37. use Symfony\Component\Security\Http\Util\TargetPathTrait;
  38. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
  39. trait LoginAuthenticatorTrait
  40. {
  41. use TargetPathTrait;
  42. protected EntityManagerInterface $em;
  43. protected UrlGeneratorInterface $urlGenerator;
  44. protected YamlReader $yamlReader;
  45. protected ConfigService $configService;
  46. protected KernelInterface $kernel;
  47. protected ServiceLocator $workflowUserServiceLocator;
  48. protected PortalService $portalService;
  49. protected SettingService $settingService;
  50. protected ModuleSettingService $moduleSettingService;
  51. protected LoggerInterface $logger;
  52. public function __construct(
  53. EntityManagerInterface $em,
  54. UrlGeneratorInterface $urlGenerator,
  55. YamlReader $yamlReader,
  56. ConfigService $configService,
  57. KernelInterface $kernel,
  58. ServiceLocator $workflowUserServiceLocator,
  59. PortalService $portalService,
  60. SettingService $settingService,
  61. ModuleSettingService $moduleSettingService,
  62. LoggerInterface $logger
  63. ) {
  64. $this->em = $em;
  65. $this->urlGenerator = $urlGenerator;
  66. $this->yamlReader = $yamlReader;
  67. $this->configService = $configService;
  68. $this->kernel = $kernel;
  69. $this->workflowUserServiceLocator = $workflowUserServiceLocator;
  70. $this->portalService = $portalService;
  71. $this->settingService = $settingService;
  72. $this->moduleSettingService = $moduleSettingService;
  73. $this->logger = $logger;
  74. }
  75. /**
  76. * @param Request $request
  77. *
  78. * @return Passport
  79. *
  80. * @throws Exception
  81. */
  82. public function authenticate(Request $request): Passport
  83. {
  84. $credentials = $request->request->all('login');
  85. $email = $credentials['email'] ?? '';
  86. $password = $credentials['password'] ?? '';
  87. // TODO PORTAL: vérifier que le module portal est activé et qu'on est bien sur un site parent et non child.
  88. if ($this->yamlReader->getType() === Platform::PORTAIL) {
  89. $user = $this->portalService->fetchUserFromApi($email, $password);
  90. if ($user instanceof User) {
  91. return new Passport(new UserBadge($email, function () use ($user) {
  92. return $user;
  93. }), new PasswordCredentials($password), [
  94. new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
  95. new RememberMeBadge()
  96. ]);
  97. }
  98. }
  99. $session = $request->getSession();
  100. $session->set(Security::LAST_USERNAME, $email);
  101. // Récupère l'utilisateur en fonction de l'e-mail
  102. $userRepo = $this->em->getRepository(User::class);
  103. $user = $userRepo->findOneByEmail($email);
  104. $loginSecurity = $this->yamlReader->getGlobal()['login_security'] ?? [];
  105. $activeDelayBeforeNewAttempt = $loginSecurity['active_delay_before_new_attempt'] ?? true;
  106. $failedAttempts = $loginSecurity['failed_attempts'] ?? 2; // !! attention !! le +1 au failedAttempts() s'ajoute après le test
  107. $delayBeforeNewAttempt = $loginSecurity['delay_before_new_attempt'] ?? 24;
  108. $passwordValidationDays = $loginSecurity['password_validation_days'] ?? 365;
  109. $env = $this->kernel->getEnvironment();
  110. if ($env !== 'test' && $user instanceof User) {
  111. // Vérifie si l'utilisateur échoue à la connexion plus de x fois et que l'option "delay" est désactivé
  112. // !! attention !! le +1 au failedAttempts() s'ajoute après le test
  113. if ($user->getFailedAttempts() > $failedAttempts && !$activeDelayBeforeNewAttempt) {
  114. throw new CustomUserMessageAuthenticationException(
  115. 'Vous avez été bloqué en raison de trop nombreuses tentatives ' . 'de connexion échouées. Veuillez cliquer sur "Mot de passe oublié ?" afin de le débloquer.',
  116. );
  117. } elseif ($user->getFailedAttempts() > $failedAttempts) {
  118. // Vérifie si l'utilisateur échoue à la connexion plus de x fois
  119. $lastFailed = $user->getLastFailedAttempt();
  120. $interval = (new DateTime())->diff($lastFailed);
  121. if ($interval->days == 0 && $interval->h < $delayBeforeNewAttempt) {
  122. throw new CustomUserMessageAuthenticationException(
  123. 'Vous avez été bloqué pendant ' . $delayBeforeNewAttempt . ' heure(s) en raison de trop nombreuses tentatives ' . 'de connexion échouées.',
  124. );
  125. } else {
  126. // Réinitialiser les tentatives après x heures
  127. $user->setFailedAttempts(0);
  128. $this->em->flush();
  129. }
  130. }
  131. $isSSO = $this->moduleSettingService->isModuleActive(Modules::SSO_CONNECTION) && $this->settingService->isExist(Setting::SSO_SETTINGS);
  132. // Vérification de la durée de validité du mot de passe
  133. if (!$user->isDeveloper() && !$isSSO) {
  134. $passwordUpdated = $user->getPasswordUpdatedAt();
  135. if (is_null($passwordUpdated)) {
  136. $passwordUpdated = (new DateTime())->modify('-' . $passwordValidationDays . ' days');
  137. $user->setPasswordUpdatedAt($passwordUpdated);
  138. $this->em->flush();
  139. }
  140. $interval = (new DateTime())->diff($passwordUpdated);
  141. if ($interval->days >= $passwordValidationDays) {
  142. // On injecte le service de workflowUser (pour éviter une dépendance circulaire dans le constructor)
  143. // On envoie un mail à l'utilisateur pour qu'il puisse changer son mot de passe et revalider les CGU
  144. try {
  145. $workflowUser = $this->workflowUserServiceLocator->get('workflowUser');
  146. $workflowUser->resendCGU($user);
  147. } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
  148. throw new Exception($e->getMessage());
  149. }
  150. throw new CustomUserMessageAuthenticationException(
  151. 'Votre mot de passe a été créé il y a plus de ' . $passwordValidationDays . ' jours, pour des raisons de sécurités, ' . 'un email vous a été envoyé afin que vous puissiez en changer. <br>' . 'Veuillez vérifier votre boite mail et suivre les instructions.',
  152. );
  153. }
  154. }
  155. }
  156. return new Passport(new UserBadge($email), new PasswordCredentials($password), [
  157. new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
  158. new RememberMeBadge()
  159. ]);
  160. }
  161. /**
  162. * @param Request $request
  163. * @param TokenInterface $token
  164. * @param string $firewallName
  165. *
  166. * @return Response|null
  167. */
  168. public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
  169. {
  170. //remise à 0 des tentatives de connexion après la connexion
  171. /** @var User $user */
  172. $user = $token->getUser();
  173. if ($user) {
  174. $user->setFailedAttempts(0);
  175. $user->setLastFailedAttempt(null);
  176. $this->em->flush();
  177. }
  178. if ($this->yamlReader->getType() === 'api') {
  179. return new RedirectResponse($this->urlGenerator->generate('back_dashboard'));
  180. }
  181. if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
  182. return new RedirectResponse($targetPath);
  183. }
  184. return new RedirectResponse($this->urlGenerator->generate('front_homepage'));
  185. }
  186. /**
  187. * @param Request $request
  188. * @param AuthenticationException $exception
  189. *
  190. * @return Response
  191. */
  192. public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
  193. {
  194. $credentials = $request->request->get('login');
  195. $email = $credentials['email'] ?? '';
  196. $userRepo = $this->em->getRepository(User::class);
  197. $user = $userRepo->findOneByEmail($email);
  198. if ($user && $exception instanceof BadCredentialsException) {
  199. $user->setFailedAttempts($user->getFailedAttempts() + 1);
  200. $user->setLastFailedAttempt(new DateTime());
  201. $this->em->flush();
  202. }
  203. if ($this->isSsoAuthenticationFailure($exception)) {
  204. $this->logger->error('Erreur lors de la connexion SSO', [
  205. 'exception_class' => $exception::class,
  206. 'exception_message' => $exception->getMessage(),
  207. 'previous_exception_class' => $exception->getPrevious() ? $exception->getPrevious()::class : null,
  208. 'previous_exception_message' => $exception->getPrevious()?->getMessage(),
  209. ]);
  210. $request->getSession()->getFlashBag()->add(
  211. 'danger',
  212. 'Une erreur est survenue lors de la connexion (SSO)'
  213. );
  214. $request->getSession()->remove(Security::AUTHENTICATION_ERROR);
  215. return new RedirectResponse($this->getLoginUrlWithQuery($request));
  216. }
  217. $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
  218. return new RedirectResponse($this->getLoginUrlWithQuery($request));
  219. }
  220. /**
  221. * @return string
  222. */
  223. protected function getLoginUrl(Request $request): string
  224. {
  225. return $this->urlGenerator->generate('app_login');
  226. }
  227. /**
  228. * @return string
  229. */
  230. protected function getLoginUrlWithQuery(Request $request): string
  231. {
  232. return $this->urlGenerator->generate('app_login', $request->query->all());
  233. }
  234. private function isSsoAuthenticationFailure(AuthenticationException $exception): bool
  235. {
  236. return $exception instanceof IdentityProviderAuthenticationException
  237. || $exception instanceof InvalidStateAuthenticationException
  238. || $exception instanceof NoAuthCodeAuthenticationException
  239. || $exception->getPrevious() instanceof IdentityProviderException;
  240. }
  241. }