<?php
namespace App\Security\Traits;
use App\Constants\Platform;
use App\Constants\Setting;
use App\Constants\Modules;
use App\Entity\User;
use App\Services\Common\ModuleSettingService;
use App\Services\Common\SettingService;
use App\Services\ConfigService;
use App\Services\DTV\YamlConfig\YamlReader;
use App\Services\Portal\PortalService;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use KnpU\OAuth2ClientBundle\Security\Exception\IdentityProviderAuthenticationException;
use KnpU\OAuth2ClientBundle\Security\Exception\InvalidStateAuthenticationException;
use KnpU\OAuth2ClientBundle\Security\Exception\NoAuthCodeAuthenticationException;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
trait LoginAuthenticatorTrait
{
use TargetPathTrait;
protected EntityManagerInterface $em;
protected UrlGeneratorInterface $urlGenerator;
protected YamlReader $yamlReader;
protected ConfigService $configService;
protected KernelInterface $kernel;
protected ServiceLocator $workflowUserServiceLocator;
protected PortalService $portalService;
protected SettingService $settingService;
protected ModuleSettingService $moduleSettingService;
protected LoggerInterface $logger;
public function __construct(
EntityManagerInterface $em,
UrlGeneratorInterface $urlGenerator,
YamlReader $yamlReader,
ConfigService $configService,
KernelInterface $kernel,
ServiceLocator $workflowUserServiceLocator,
PortalService $portalService,
SettingService $settingService,
ModuleSettingService $moduleSettingService,
LoggerInterface $logger
) {
$this->em = $em;
$this->urlGenerator = $urlGenerator;
$this->yamlReader = $yamlReader;
$this->configService = $configService;
$this->kernel = $kernel;
$this->workflowUserServiceLocator = $workflowUserServiceLocator;
$this->portalService = $portalService;
$this->settingService = $settingService;
$this->moduleSettingService = $moduleSettingService;
$this->logger = $logger;
}
/**
* @param Request $request
*
* @return Passport
*
* @throws Exception
*/
public function authenticate(Request $request): Passport
{
$credentials = $request->request->all('login');
$email = $credentials['email'] ?? '';
$password = $credentials['password'] ?? '';
// TODO PORTAL: vérifier que le module portal est activé et qu'on est bien sur un site parent et non child.
if ($this->yamlReader->getType() === Platform::PORTAIL) {
$user = $this->portalService->fetchUserFromApi($email, $password);
if ($user instanceof User) {
return new Passport(new UserBadge($email, function () use ($user) {
return $user;
}), new PasswordCredentials($password), [
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
new RememberMeBadge()
]);
}
}
$session = $request->getSession();
$session->set(Security::LAST_USERNAME, $email);
// Récupère l'utilisateur en fonction de l'e-mail
$userRepo = $this->em->getRepository(User::class);
$user = $userRepo->findOneByEmail($email);
$loginSecurity = $this->yamlReader->getGlobal()['login_security'] ?? [];
$activeDelayBeforeNewAttempt = $loginSecurity['active_delay_before_new_attempt'] ?? true;
$failedAttempts = $loginSecurity['failed_attempts'] ?? 2; // !! attention !! le +1 au failedAttempts() s'ajoute après le test
$delayBeforeNewAttempt = $loginSecurity['delay_before_new_attempt'] ?? 24;
$passwordValidationDays = $loginSecurity['password_validation_days'] ?? 365;
$env = $this->kernel->getEnvironment();
if ($env !== 'test' && $user instanceof User) {
// Vérifie si l'utilisateur échoue à la connexion plus de x fois et que l'option "delay" est désactivé
// !! attention !! le +1 au failedAttempts() s'ajoute après le test
if ($user->getFailedAttempts() > $failedAttempts && !$activeDelayBeforeNewAttempt) {
throw new CustomUserMessageAuthenticationException(
'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.',
);
} elseif ($user->getFailedAttempts() > $failedAttempts) {
// Vérifie si l'utilisateur échoue à la connexion plus de x fois
$lastFailed = $user->getLastFailedAttempt();
$interval = (new DateTime())->diff($lastFailed);
if ($interval->days == 0 && $interval->h < $delayBeforeNewAttempt) {
throw new CustomUserMessageAuthenticationException(
'Vous avez été bloqué pendant ' . $delayBeforeNewAttempt . ' heure(s) en raison de trop nombreuses tentatives ' . 'de connexion échouées.',
);
} else {
// Réinitialiser les tentatives après x heures
$user->setFailedAttempts(0);
$this->em->flush();
}
}
$isSSO = $this->moduleSettingService->isModuleActive(Modules::SSO_CONNECTION) && $this->settingService->isExist(Setting::SSO_SETTINGS);
// Vérification de la durée de validité du mot de passe
if (!$user->isDeveloper() && !$isSSO) {
$passwordUpdated = $user->getPasswordUpdatedAt();
if (is_null($passwordUpdated)) {
$passwordUpdated = (new DateTime())->modify('-' . $passwordValidationDays . ' days');
$user->setPasswordUpdatedAt($passwordUpdated);
$this->em->flush();
}
$interval = (new DateTime())->diff($passwordUpdated);
if ($interval->days >= $passwordValidationDays) {
// On injecte le service de workflowUser (pour éviter une dépendance circulaire dans le constructor)
// On envoie un mail à l'utilisateur pour qu'il puisse changer son mot de passe et revalider les CGU
try {
$workflowUser = $this->workflowUserServiceLocator->get('workflowUser');
$workflowUser->resendCGU($user);
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
throw new Exception($e->getMessage());
}
throw new CustomUserMessageAuthenticationException(
'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.',
);
}
}
}
return new Passport(new UserBadge($email), new PasswordCredentials($password), [
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
new RememberMeBadge()
]);
}
/**
* @param Request $request
* @param TokenInterface $token
* @param string $firewallName
*
* @return Response|null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
//remise à 0 des tentatives de connexion après la connexion
/** @var User $user */
$user = $token->getUser();
if ($user) {
$user->setFailedAttempts(0);
$user->setLastFailedAttempt(null);
$this->em->flush();
}
if ($this->yamlReader->getType() === 'api') {
return new RedirectResponse($this->urlGenerator->generate('back_dashboard'));
}
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('front_homepage'));
}
/**
* @param Request $request
* @param AuthenticationException $exception
*
* @return Response
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
$credentials = $request->request->get('login');
$email = $credentials['email'] ?? '';
$userRepo = $this->em->getRepository(User::class);
$user = $userRepo->findOneByEmail($email);
if ($user && $exception instanceof BadCredentialsException) {
$user->setFailedAttempts($user->getFailedAttempts() + 1);
$user->setLastFailedAttempt(new DateTime());
$this->em->flush();
}
if ($this->isSsoAuthenticationFailure($exception)) {
$this->logger->error('Erreur lors de la connexion SSO', [
'exception_class' => $exception::class,
'exception_message' => $exception->getMessage(),
'previous_exception_class' => $exception->getPrevious() ? $exception->getPrevious()::class : null,
'previous_exception_message' => $exception->getPrevious()?->getMessage(),
]);
$request->getSession()->getFlashBag()->add(
'danger',
'Une erreur est survenue lors de la connexion (SSO)'
);
$request->getSession()->remove(Security::AUTHENTICATION_ERROR);
return new RedirectResponse($this->getLoginUrlWithQuery($request));
}
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse($this->getLoginUrlWithQuery($request));
}
/**
* @return string
*/
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate('app_login');
}
/**
* @return string
*/
protected function getLoginUrlWithQuery(Request $request): string
{
return $this->urlGenerator->generate('app_login', $request->query->all());
}
private function isSsoAuthenticationFailure(AuthenticationException $exception): bool
{
return $exception instanceof IdentityProviderAuthenticationException
|| $exception instanceof InvalidStateAuthenticationException
|| $exception instanceof NoAuthCodeAuthenticationException
|| $exception->getPrevious() instanceof IdentityProviderException;
}
}