src/Domain/User/Controller/SecurityController.php line 184

Open in your IDE?
  1. <?php
  2. /**
  3.  * This file is part of the MADIS - RGPD Management application.
  4.  *
  5.  * @copyright Copyright (c) 2018-2019 Soluris - Solutions Numériques Territoriales Innovantes
  6.  * @author Donovan Bourlard <donovan@awkan.fr>
  7.  *
  8.  * This program is free software: you can redistribute it and/or modify
  9.  * it under the terms of the GNU Affero General Public License as published by
  10.  * the Free Software Foundation, either version 3 of the License, or
  11.  * (at your option) any later version.
  12.  *
  13.  * This program is distributed in the hope that it will be useful,
  14.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  16.  * GNU Affero General Public License for more details.
  17.  *
  18.  * You should have received a copy of the GNU Affero General Public License
  19.  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  20.  */
  21. declare(strict_types=1);
  22. namespace App\Domain\User\Controller;
  23. use App\Application\Controller\ControllerHelper;
  24. use App\Application\Symfony\Security\UserProvider;
  25. use App\Domain\User\Component\Mailer;
  26. use App\Domain\User\Component\TokenGenerator;
  27. use App\Domain\User\Form\Type\ResetPasswordType;
  28. use App\Domain\User\Model\User;
  29. use App\Domain\User\Repository;
  30. use Doctrine\ORM\EntityManagerInterface;
  31. use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
  32. use KnpU\OAuth2ClientBundle\Client\OAuth2Client;
  33. use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
  34. use Psr\Log\LoggerInterface;
  35. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  36. use Symfony\Component\HttpFoundation\RedirectResponse;
  37. use Symfony\Component\HttpFoundation\Request;
  38. use Symfony\Component\HttpFoundation\Response;
  39. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  40. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  41. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  42. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  43. use Twig\Error\LoaderError;
  44. use Twig\Error\RuntimeError;
  45. use Twig\Error\SyntaxError;
  46. class SecurityController extends AbstractController
  47. {
  48.     private ControllerHelper $helper;
  49.     private AuthenticationUtils $authenticationUtils;
  50.     private TokenGenerator $tokenGenerator;
  51.     private Repository\User $userRepository;
  52.     private Mailer $mailer;
  53.     private UserProvider $userProvider;
  54.     private EntityManagerInterface $entityManager;
  55.     private ?string $sso_type;
  56.     private ?string $sso_key_field;
  57.     public function __construct(
  58.         ControllerHelper $helper,
  59.         AuthenticationUtils $authenticationUtils,
  60.         TokenGenerator $tokenGenerator,
  61.         Repository\User $userRepository,
  62.         Mailer $mailer,
  63.         UserProvider $userProvider,
  64.         EntityManagerInterface $entityManager,
  65.         ?string $sso_type,
  66.         ?string $sso_key_field
  67.     ) {
  68.         $this->helper              $helper;
  69.         $this->authenticationUtils $authenticationUtils;
  70.         $this->tokenGenerator      $tokenGenerator;
  71.         $this->userRepository      $userRepository;
  72.         $this->mailer              $mailer;
  73.         $this->userProvider        $userProvider;
  74.         $this->entityManager       $entityManager;
  75.         $this->sso_type            $sso_type;
  76.         $this->sso_key_field       $sso_key_field;
  77.     }
  78.     /**
  79.      * Display login page.
  80.      *
  81.      * @throws LoaderError
  82.      * @throws RuntimeError
  83.      * @throws SyntaxError
  84.      */
  85.     public function loginAction(): Response
  86.     {
  87.         $error        $this->authenticationUtils->getLastAuthenticationError();
  88.         $lastUsername $this->authenticationUtils->getLastUsername();
  89.         return $this->helper->render('User/Security/login.html.twig', [
  90.             'last_username' => $lastUsername,
  91.             'error'         => $error,
  92.             'sso_type'      => $this->sso_type,
  93.         ]);
  94.     }
  95.     public function oauthConnectAction(Request $requestClientRegistry $clientRegistry): RedirectResponse
  96.     {
  97.         $currentUser $this->userProvider->getAuthenticatedUser();
  98.         if ($currentUser && null !== $currentUser->getSsoKey()) {
  99.             return $this->_handleUserLoggedAlreadyAssociated();
  100.         }
  101.         $oauthServiceName $request->get('service');
  102.         try {
  103.             $client $clientRegistry->getClient($oauthServiceName);
  104.         } catch (\Exception) {
  105.             return $this->_handleSsoClientError();
  106.         }
  107.         // scope openid required to get id_token needed for logout
  108.         return $client->redirect(['openid'], []);
  109.     }
  110.     public function oauthCheckAction(Request $requestClientRegistry $clientRegistryTokenStorageInterface $tokenStorageLoggerInterface $logger): RedirectResponse
  111.     {
  112.         $oauthServiceName $request->get('service');
  113.         /** @var OAuth2Client $client */
  114.         $client $clientRegistry->getClient($oauthServiceName);
  115.         try {
  116.             // scope openid required to get id_token needed for logout
  117.             $accessToken   $client->getAccessToken(['scope' => 'openid']);
  118.             $userOAuthData $client->fetchUserFromToken($accessToken)->toArray();
  119.         } catch (IdentityProviderException) {
  120.             return $this->_handleSsoClientError();
  121.         }
  122.         $sso_key_field $this->sso_key_field;
  123.         try {
  124.             $sso_value $userOAuthData[$sso_key_field];
  125.         } catch (\Exception) {
  126.             $logger->error('SSO field "' $sso_key_field '" not found.');
  127.             $logger->info('Data returned by SSO: ' json_encode($userOAuthData));
  128.             return $this->_handleSsoClientError();
  129.         }
  130.         $ssoLogoutUrl null;
  131.         $provider     $client->getOAuth2Provider();
  132.         if (method_exists($provider'getLogoutUrl')) {
  133.             $tokenValues  $accessToken->getValues();
  134.             $ssoLogoutUrl $provider->getLogoutUrl([
  135.                 'id_token_hint'            => $tokenValues['id_token'],
  136.                 'post_logout_redirect_uri' => $this->generateUrl('login', [], UrlGeneratorInterface::ABSOLUTE_URL),
  137.             ]);
  138.             $session $request->getSession();
  139.             $session->set('ssoLogoutUrl'$ssoLogoutUrl);
  140.         }
  141.         $currentUser $this->userProvider->getAuthenticatedUser();
  142.         if ($currentUser) {
  143.             return $this->_associateUserWithSsoKey($sso_value$currentUser);
  144.         }
  145.         $user $this->userRepository->findOneOrNullBySsoKey($sso_value);
  146.         if (!$user) {
  147.             return $this->_handleUserNotFound($ssoLogoutUrl);
  148.         }
  149.         // Login Programmatically
  150.         $token = new UsernamePasswordToken($user$user->getPassword(), 'public'$user->getRoles());
  151.         $tokenStorage->setToken($token);
  152.         return $this->helper->redirectToRoute('reporting_dashboard_index');
  153.     }
  154.     /**
  155.      * Display Forget password page.
  156.      *
  157.      * @throws LoaderError
  158.      * @throws RuntimeError
  159.      * @throws SyntaxError
  160.      */
  161.     public function forgetPasswordAction(): Response
  162.     {
  163.         return $this->helper->render('User/Security/forget_password.html.twig');
  164.     }
  165.     /**
  166.      * Forget password confirmation
  167.      * - Check if email exists on DB (redirect to forgetPasswordAction is not exists)
  168.      * - Generate user token
  169.      * - Send forget password email
  170.      * - Display forget password confirmation page.
  171.      *
  172.      * @throws RuntimeError
  173.      * @throws SyntaxError
  174.      * @throws LoaderError
  175.      */
  176.     public function forgetPasswordConfirmAction(Request $request): Response
  177.     {
  178.         $email $request->request->get('email');
  179.         $user  $this->userRepository->findOneOrNullByEmail($email);
  180.         if (!$user) {
  181.             return $this->helper->render('User/Security/forget_password_confirm.html.twig');
  182.         }
  183.         $user->setForgetPasswordToken($this->tokenGenerator->generateToken());
  184.         $this->userRepository->update($user);
  185.         $this->mailer->sendForgetPassword($user);
  186.         return $this->helper->render('User/Security/forget_password_confirm.html.twig');
  187.     }
  188.     /**
  189.      * Reset user password
  190.      * - Token does not exists in DB: redirect to login page with flashbag error
  191.      * - Token exists in DB: show reset password form for related user.
  192.      *
  193.      * @param Request $request The Request
  194.      * @param string  $token   The forgetPasswordToken to search the user with
  195.      *
  196.      * @throws LoaderError
  197.      * @throws RuntimeError
  198.      * @throws SyntaxError
  199.      */
  200.     public function resetPasswordAction(Request $requeststring $token): Response
  201.     {
  202.         $user $this->userRepository->findOneOrNullByForgetPasswordToken($token);
  203.         // If user doesn't exists, add flashbag error & return to login page
  204.         if (!$user) {
  205.             return $this->_handleUserNotFound();
  206.         }
  207.         // User exist, display reset password form
  208.         $form $this->helper->createForm(ResetPasswordType::class, $user);
  209.         $form->handleRequest($request);
  210.         if ($form->isSubmitted() && $form->isValid()) {
  211.             // Remove forgetPasswordToken on user since password is not reset
  212.             $user->setForgetPasswordToken(null);
  213.             $this->userRepository->update($user);
  214.             $this->helper->addFlash('success'$this->helper->trans('user.security.reset_password.flashbag.success'));
  215.             return $this->helper->redirectToRoute('login');
  216.         }
  217.         return $this->helper->render('User/Security/reset_password.html.twig', [
  218.             'form' => $form->createView(),
  219.         ]);
  220.     }
  221.     private function _handleUserLoggedAlreadyAssociated(): RedirectResponse
  222.     {
  223.         $this->helper->addFlash('warning',
  224.             $this->helper->trans('user.profile.flashbag.error.sso_already_associated')
  225.         );
  226.         return $this->helper->redirectToRoute('user_profile_user_edit');
  227.     }
  228.     private function _handleSsoClientError(): RedirectResponse
  229.     {
  230.         $this->helper->addFlash('danger',
  231.             $this->helper->trans('user.security.sso_login.flashbag.sso_client_error')
  232.         );
  233.         return $this->helper->redirectToRoute('login');
  234.     }
  235.     private function _handleUserNotFound(string $logoutUrl null): RedirectResponse
  236.     {
  237.         $this->helper->addFlash(
  238.             'danger',
  239.             $this->helper->trans('user.security.reset_password.flashbag.error')
  240.         );
  241.         if ($logoutUrl) {
  242.             return $this->redirect($logoutUrl);
  243.         }
  244.         return $this->helper->redirectToRoute('login');
  245.     }
  246.     private function _handleDuplicateUserWithSsoKey(User $alreadyExists): RedirectResponse
  247.     {
  248.         $this->helper->addFlash('danger',
  249.             $this->helper->trans('user.profile.flashbag.error.sso_key_duplicate', ['email' => $alreadyExists->getEmail()])
  250.         );
  251.         return $this->helper->redirectToRoute('user_profile_user_edit');
  252.     }
  253.     private function _associateUserWithSsoKey(mixed $sso_valueUser $currentUser): RedirectResponse
  254.     {
  255.         $alreadyExists $this->userRepository->findOneOrNullBySsoKey($sso_value);
  256.         if ($alreadyExists) {
  257.             return $this->_handleDuplicateUserWithSsoKey($alreadyExists);
  258.         }
  259.         // associate user with sso key
  260.         $currentUser->setSsoKey($sso_value);
  261.         $this->entityManager->persist($currentUser);
  262.         $this->entityManager->flush();
  263.         $this->helper->addFlash('success',
  264.             $this->helper->trans('user.profile.flashbag.success.sso_associated')
  265.         );
  266.         return $this->helper->redirectToRoute('user_profile_user_edit');
  267.     }
  268. }