src/Controller/SecurityController.php line 140

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Constants\Setting;
  4. use App\Constants\Setting as SettingConst;
  5. use App\Constants\Modules;
  6. use App\Constants\Synchronisation;
  7. use App\Constants\Sso;
  8. use App\Entity\User;
  9. use App\Exception\EncryptionException;
  10. use App\Factory\Security\SecurityFormFactory;
  11. use App\Form\Type\LoginType;
  12. use App\Services\Common\ModuleSettingService;
  13. use App\Services\Common\SettingService;
  14. use App\Services\Common\User\WorkflowUser;
  15. use App\Services\Common\UserService;
  16. use App\Services\DTV\YamlConfig\YamlReader;
  17. use App\Services\Security\EncryptionManager;
  18. use App\Services\Security\RegisterService;
  19. use DateTime;
  20. use Doctrine\ORM\EntityManagerInterface;
  21. use Exception;
  22. use LogicException;
  23. use Psr\Container\ContainerExceptionInterface;
  24. use Psr\Container\NotFoundExceptionInterface;
  25. use Psr\Log\LoggerInterface;
  26. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  27. use Symfony\Component\HttpFoundation\RedirectResponse;
  28. use Symfony\Component\HttpFoundation\Request;
  29. use Symfony\Component\HttpFoundation\Response;
  30. use Symfony\Component\Routing\Annotation\Route;
  31. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  32. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  33. use Symfony\Component\Translation\TranslatableMessage;
  34. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  35. use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
  36. /**
  37. * Controller qui gère la sécurité
  38. */
  39. class SecurityController extends AbstractController
  40. {
  41. private YamlReader $yamlReader;
  42. private SecurityFormFactory $formFactory;
  43. private SettingService $settingService;
  44. private EntityManagerInterface $em;
  45. private RegisterService $registerService;
  46. private EncryptionManager $encryptionManager;
  47. private WorkflowUser $workflowUser;
  48. private LoggerInterface $logger;
  49. private string $env;
  50. private UserService $userService;
  51. private ModuleSettingService $moduleSettingService;
  52. public function __construct(
  53. YamlReader $yamlReader,
  54. SecurityFormFactory $formFactory,
  55. SettingService $settingService,
  56. EntityManagerInterface $em,
  57. RegisterService $registerService,
  58. EncryptionManager $encryptionManager,
  59. WorkflowUser $workflowUser,
  60. LoggerInterface $logger,
  61. string $env,
  62. UserService $userService,
  63. ModuleSettingService $moduleSettingService
  64. ) {
  65. $this->yamlReader = $yamlReader;
  66. $this->formFactory = $formFactory;
  67. $this->settingService = $settingService;
  68. $this->em = $em;
  69. $this->registerService = $registerService;
  70. $this->encryptionManager = $encryptionManager;
  71. $this->workflowUser = $workflowUser;
  72. $this->logger = $logger;
  73. $this->env = $env;
  74. $this->userService = $userService;
  75. $this->moduleSettingService = $moduleSettingService;
  76. }
  77. /**
  78. * Formulaire de connexion
  79. *
  80. * @Route("/login", name="app_login")
  81. *
  82. * @param AuthenticationUtils $authenticationUtils
  83. * @param Request $request
  84. *
  85. * @return Response
  86. *
  87. * @throws EncryptionException
  88. * @throws ResetPasswordExceptionInterface
  89. * @throws TransportExceptionInterface
  90. * @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
  91. */
  92. public function login(AuthenticationUtils $authenticationUtils, Request $request): Response
  93. {
  94. if ($this->getUser() instanceof User || $request->query->has('q') || $request->query->has(Synchronisation::LOGIN_SKIP_QUERY_PARAMETER)) {
  95. return $this->processLogin($authenticationUtils, $request);
  96. }
  97. if ($this->shouldStartSynchronizationOnLogin($request)) {
  98. return new RedirectResponse($this->generateUrl('synchronisation_start'));
  99. }
  100. return $this->processLogin($authenticationUtils, $request);
  101. }
  102. /**
  103. * Formulaire de connexion secondaire
  104. *
  105. * @Route("/login-admin", name="app_login_admin")
  106. *
  107. * @param AuthenticationUtils $authenticationUtils
  108. * @param Request $request
  109. *
  110. * @return Response
  111. * @throws EncryptionException
  112. * @throws ResetPasswordExceptionInterface
  113. * @throws TransportExceptionInterface
  114. * @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
  115. */
  116. public function loginAdmin(AuthenticationUtils $authenticationUtils, Request $request): Response
  117. {
  118. if ($this->settingService->isExist(Setting::SSO_SETTINGS) && $this->moduleSettingService->isModuleActive(Modules::SSO_CONNECTION)) {
  119. return $this->processLogin($authenticationUtils, $request, 'security/login_admin.html.twig');
  120. }
  121. throw $this->createNotFoundException();
  122. }
  123. /**
  124. * @throws TransportExceptionInterface
  125. * @throws ResetPasswordExceptionInterface
  126. * @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
  127. * @throws EncryptionException
  128. * @throws Exception
  129. */
  130. protected function processLogin(
  131. AuthenticationUtils $authenticationUtils,
  132. Request $request,
  133. ?string $twigPath = null
  134. ): Response {
  135. // On gère ici si on est sur un Portail/enfant
  136. $setting = $this->settingService->getSettingFromName(SettingConst::PORTAL_CHILDREN);
  137. $values = $setting !== null ? json_decode($setting->getValue(), true) : [];
  138. $hasParent = !empty($values['parent_url']);
  139. if ($hasParent) {
  140. $header = $request->headers;
  141. if ($header->has('q') || $request->query->get('q')) {
  142. $q = base64_decode($header->get('q') ?? $request->query->get('q'));
  143. try {
  144. $q = $this->encryptionManager->decrypt($q);
  145. } catch (Exception $e) {
  146. $this->addFlash('danger', 'Un problème est survenu lors de la connexion');
  147. $this->logger->error(
  148. 'Erreur lors du décryptage du token de connexion',
  149. ['error' => $e->getMessage()]
  150. );
  151. return $this->redirectToRoute('app_login', $request->query->has(Synchronisation::LOGIN_SKIP_QUERY_PARAMETER) ? [Synchronisation::LOGIN_SKIP_QUERY_PARAMETER => 1] : []);
  152. }
  153. $q = json_decode($q, true);
  154. $user = $this->em->getRepository(User::class)->findOneByEmailOrExtension([
  155. 'email' => $q['email'],
  156. 'extension1' => $q['extension1'],
  157. 'extension2' => $q['extension2'],
  158. ]);
  159. if ($user instanceof User) {
  160. if ($q['email'] !== $user->getEmail()) {
  161. $user
  162. ->setEmail($q['email'])
  163. ->setFirstname($q['firstName'])
  164. ->setLastname($q['lastName'])
  165. ->setRoles($q['roles']);
  166. $this->registerService->registerReplaceFakeUserBySellerCode($user, $q['extension1'], true);
  167. }
  168. // Gestion du cas où l'utilisateur existe en BDD (pas en fakeUser) mais ne s'est pas encore connecté à la plateforme
  169. if ($q['cguAt'] !== null && $user->getStatus() === 'cgu_pending') {
  170. $this->workflowUser->acceptCGU($user);
  171. $user->setCguAt(new DateTime($q['cguAt']));
  172. $this->em->flush();
  173. }
  174. // Créer un token de connexion
  175. $token = new UsernamePasswordToken($user, 'app', $user->getRoles());
  176. // Stocker le token dans le token storage
  177. try {
  178. $this->container->get('security.token_storage')->setToken($token);
  179. } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
  180. $this->addFlash('danger', 'Un problème est survenu lors de la connexion');
  181. $this->logger->error(
  182. 'Erreur lors de la récupération du token storage',
  183. ['error' => $e->getMessage()]
  184. );
  185. return $this->redirectToRoute('front_homepage');
  186. }
  187. } else {
  188. // on delog l'user
  189. $this->get('security.token_storage')->setToken(null);
  190. $this->logger->info('L\'utilisateur n\'existe pas en BDD', ['email' => $q['email']]);
  191. $request->getSession()->invalidate();
  192. }
  193. }
  194. }
  195. if ($this->getUser()) {
  196. return $this->redirectToRoute('front_homepage');
  197. }
  198. $user = $this->userService->initUser();
  199. $config = $this->yamlReader->getFrontSecurity();
  200. $configLogin = $config['login'];
  201. $globalRegister = $this->yamlReader->getRegister();
  202. $globalRegisterEnabled = $globalRegister['enabled'];
  203. $configRegister = $configLogin['sections']['section_register'] ?? false;
  204. $hasFormRegister = false;
  205. $formRegister = false;
  206. if ($globalRegisterEnabled && is_array($configRegister) && $configRegister['enabled']) {
  207. // Création du formulaire d'inscription
  208. try {
  209. $formRegister = $this->formFactory->generateRegisterForm($user);
  210. $hasFormRegister = true;
  211. } catch (Exception $e) {
  212. throw $this->createNotFoundException($e->getMessage());
  213. }
  214. $formRegister->handleRequest($request);
  215. if ($formRegister->isSubmitted()) {
  216. // Validation spécifique du formulaire d'inscription
  217. try {
  218. $formRegister = $this->formFactory->postValidateRegisterForm($formRegister);
  219. } catch (Exception $e) {
  220. if ($this->env != 'prod') {
  221. throw $e;
  222. }
  223. $this->addFlash(
  224. 'danger',
  225. new TranslatableMessage('impossible d\'exécuter la post validation du formulaire', [])
  226. );
  227. $this->logger->error(
  228. 'Erreur lors de la post validation du formulaire d\'inscription',
  229. ['error' => $e->getMessage()]
  230. );
  231. $referer = $request->headers->get('referer');
  232. return $this->redirect($referer);
  233. }
  234. if ($formRegister->isValid()) {
  235. // Post traitement du formulaire d'inscription
  236. try {
  237. $response = $this->formFactory->postProcessingRegisterForm($formRegister, $user);
  238. } catch (Exception $e) {
  239. if ($this->env != 'prod') {
  240. throw $e;
  241. }
  242. $this->addFlash('danger', 'impossible d\'exécuter le post traitement du formulaire');
  243. $this->logger->error(
  244. 'Erreur lors du post traitement du formulaire d\'inscription',
  245. ['error' => $e->getMessage()]
  246. );
  247. $referer = $request->headers->get('referer');
  248. return $this->redirect($referer);
  249. } catch (TransportExceptionInterface $e) {
  250. if ($this->env != 'prod') {
  251. throw $e;
  252. }
  253. $this->addFlash('danger', 'impossible d\'exécuter le post traitement du formulaire');
  254. $this->logger->error(
  255. 'Erreur lors du post traitement du formulaire d\'inscription',
  256. ['error' => $e->getMessage()]
  257. );
  258. $referer = $request->headers->get('referer');
  259. return $this->redirect($referer);
  260. }
  261. if ($response['message'] !== null) {
  262. $this->addFlash('success', $response['message']);
  263. }
  264. return $this->redirectToRoute($response['route']);
  265. }
  266. }
  267. }
  268. $formLogin = $this->createForm(LoginType::class);
  269. // get the login error if there is one
  270. $error = $authenticationUtils->getLastAuthenticationError();
  271. // last username entered by the user
  272. $lastUsername = $authenticationUtils->getLastUsername();
  273. if (!$twigPath) {
  274. $twigPath = 'security/login.html.twig';
  275. if (isset($configLogin['folder']) && !in_array($configLogin['folder'], [false, '', null], true)) {
  276. $twigPath = 'security/' . $configLogin['folder'] . '/login.html.twig';
  277. }
  278. }
  279. return $this->render($twigPath, [
  280. 'last_username' => $lastUsername,
  281. 'error' => $error,
  282. 'loginForm' => $formLogin->createView(),
  283. 'registrationForm' => $hasFormRegister ? $formRegister->createView() : false,
  284. 'hasFormRegister' => $hasFormRegister
  285. ]);
  286. }
  287. /**
  288. * Déconnexion
  289. *
  290. * @Route("/universal_logout", name="universal_logout")
  291. *
  292. * @return RedirectResponse
  293. */
  294. public function universalLogout(): RedirectResponse
  295. {
  296. if ($this->isSamlSsoEnabled()) {
  297. return $this->redirectToRoute('saml_logout');
  298. }
  299. return $this->redirectToRoute('app_logout');
  300. }
  301. /**
  302. * Déconnexion
  303. *
  304. * @Route("/logout", name="app_logout")
  305. *
  306. * @return void
  307. */
  308. public function logout(): void
  309. {
  310. throw new LogicException(
  311. 'This method can be blank - it will be intercepted by the logout key on your firewall.',
  312. );
  313. }
  314. private function isSamlSsoEnabled(): bool
  315. {
  316. if (!$this->moduleSettingService->isModuleActive(Modules::SSO_CONNECTION) || !$this->settingService->isExist(Setting::SSO_SETTINGS)) {
  317. return false;
  318. }
  319. $ssoSettings = $this->settingService->getSettingFromName(Setting::SSO_SETTINGS, true, true);
  320. return is_array($ssoSettings) && !empty($ssoSettings['ssoType']) && strtolower((string)$ssoSettings['ssoType']) === Sso::SSO_SAML;
  321. }
  322. private function shouldStartSynchronizationOnLogin(Request $request): bool
  323. {
  324. if (!$request->isMethod(Request::METHOD_GET)) {
  325. return false;
  326. }
  327. if ($request->query->has(Synchronisation::LOGIN_SKIP_QUERY_PARAMETER)) {
  328. return false;
  329. }
  330. return $this->moduleSettingService->isModuleActive(Modules::SYNCHRONISATION) && $this->settingService->isExist(Setting::SYNCHRONISATION_SETTINGS);
  331. }
  332. }