src/Controller/User/SecurityController.php line 67

Open in your IDE?
  1. <?php
  2. namespace App\Controller\User;
  3. use App\Entity\User\User;
  4. use App\Exception\Core\SSO\NoTokenException;
  5. use App\Security\Firewall\DefaultFirewall;
  6. use App\Services\User\LoginManager;
  7. use App\Services\Core\AuthService;
  8. use App\Services\Core\CreateDemoClassroom;
  9. use App\Services\Core\EventLogger;
  10. use App\Services\Core\SelfStudyVoterService;
  11. use App\Services\Core\TextbookVoterService;
  12. use DateTime;
  13. use Doctrine\ORM\EntityManagerInterface;
  14. use GuzzleHttp\Exception\RequestException;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  17. use Symfony\Component\HttpFoundation\RedirectResponse;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\HttpFoundation\RequestStack;
  20. use Symfony\Component\HttpFoundation\Response;
  21. use Symfony\Component\Routing\Annotation\Route;
  22. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  23. use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
  24. use Symfony\Component\Security\Core\Security;
  25. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  26. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  27. use Symfony\Component\Security\Http\Util\TargetPathTrait;
  28. class SecurityController extends AbstractController
  29. {
  30. use TargetPathTrait;
  31. private Request $request;
  32. public function __construct(
  33. private readonly EntityManagerInterface $entityManager,
  34. private readonly EventLogger $eventLogger,
  35. private readonly TextbookVoterService $textbookVoterService,
  36. private readonly SelfStudyVoterService $selfStudyVoterService,
  37. private readonly LoginManager $loginManager,
  38. private readonly CreateDemoClassroom $createDemoClassroom,
  39. private readonly AuthService $authService,
  40. private readonly CsrfTokenManagerInterface $tokenManager,
  41. private readonly RequestStack $requestStack,
  42. private readonly AuthenticationUtils $authenticationUtils,
  43. private readonly ParameterBagInterface $parameterBag
  44. )
  45. {
  46. }
  47. // route : /cms or /testlogin redirects to /login?alternative_login=1
  48. #[Route('/testlogin', name: 'alternative_login')]
  49. #[Route('/cms', name: 'alternative_login_cms')]
  50. public function alternativeLogin(Request $request): RedirectResponse
  51. {
  52. $request->getSession()->set('login_method', 'alternative');
  53. return $this->redirectToRoute("login", [
  54. "alternative_login" => true,
  55. ]);
  56. }
  57. // route /login
  58. #[Route('/login', name: 'login')]
  59. public function loginAction(): Response
  60. {
  61. $this->request = $this->requestStack->getCurrentRequest();
  62. $session = $this->request->getSession();
  63. $authErrorKey = Security::AUTHENTICATION_ERROR;
  64. // get the error if any (works with forward and redirect -- see below)
  65. if ($this->request->attributes->has($authErrorKey)) {
  66. $error = $this->request->attributes->get($authErrorKey);
  67. } elseif (null !== $session && $session->has($authErrorKey)) {
  68. $error = $session->get($authErrorKey);
  69. $session->remove($authErrorKey);
  70. } else {
  71. $error = null;
  72. }
  73. if (!$error instanceof AuthenticationException) {
  74. $error = null; // The value does not come from the security component.
  75. }
  76. $csrfToken = $this->tokenManager->getToken('authenticate')->getValue();
  77. $environment = $this->parameterBag->get('abacus_environment');
  78. $data = [
  79. 'error' => $error,
  80. 'csrf_token' => $csrfToken,
  81. 'environment' => $environment
  82. ];
  83. if ($this->shouldShowAlternativeLogin($error)) {
  84. return $this->render('User/Security/alternative_login.html.twig', $data);
  85. }
  86. return $this->render('User/Security/skip_to_unilogin.html.twig', $data);
  87. }
  88. #[Route('/user/unilogin', name: 'uni_login_return_route')]
  89. public function uniLoginReturn()
  90. {
  91. $ssoProvider = $this->authService->getSsoProvider();
  92. try {
  93. $ssoProvider->initialize();
  94. if (!$ssoProvider->isValidToken()) {
  95. throw $this->createAccessDeniedException('The UNI-login token is invalid.');
  96. }
  97. /* Validate license primaryschool */
  98. if ($this->getParameter('abacus.loginconnector.require_license') == 1) {
  99. if (!$ssoProvider->checkIfUserHasAccess()) {
  100. return $this->render('User/Security/nolicence_kvik.html.twig', []);
  101. }
  102. }
  103. } catch (RequestException | NoTokenException $e) {
  104. return $this->render('User/Security/timeout_error.html.twig');
  105. }
  106. // Find UNI user
  107. /** @var User $user */
  108. $user = $ssoProvider->findUserIfExists();
  109. if ($user instanceof User) {
  110. /* Existing user */
  111. /* Update institution and role */
  112. /** @var User $user */
  113. $user = $ssoProvider->updateUser($user);
  114. $user->setLastLoginWithUnilogin(new DateTime()); // save timestamp
  115. $this->eventLogger->log('login_' . $_SERVER['HTTP_USER_AGENT'], $user);
  116. }
  117. else {
  118. /* New user */
  119. /** @var User $user */
  120. $user = $ssoProvider->generateUser();
  121. $user->setLastLoginWithUnilogin(new DateTime()); // save timestamp
  122. $this->eventLogger->log('createuser', $user, $user->getInstitution()->getName());
  123. }
  124. $this->persistUser($user);
  125. /* Validate license for highschool */
  126. if ($this->getParameter('abacus_environment') === 'highschool') {
  127. if ($user->hasRole(User::role_teacher)) {
  128. $isTemporarilyGrantedAccess = $this->getParameter('abacus.systime.require_license') === 0;
  129. if (!$isTemporarilyGrantedAccess && !$ssoProvider->canHighschoolTeacherAccess($user)) {
  130. return $this->render('User/Security/nolicense_abacus.html.twig', [
  131. 'username' => $user->getUsername()
  132. ]);
  133. }
  134. }
  135. }
  136. return $this->loginUniUser($user);
  137. }
  138. /* Login existing user */
  139. private function loginUniUser(User $user): RedirectResponse
  140. {
  141. $env = $this->getParameter('abacus_environment');
  142. try {
  143. if ($user->hasRole(User::role_student) && $env=="primaryschool") {
  144. if (!is_numeric($user->getClassLevel())) {
  145. $user->setClassLevel(9);
  146. }
  147. }
  148. else if ($user->hasRole(User::role_student) && $env === "highschool") {
  149. $this->textbookVoterService->updateAccess($user);
  150. $this->selfStudyVoterService->updateAccess($user);
  151. }
  152. } catch (\Exception $e) {
  153. }
  154. $response = $this->redirectToRoute('login_redirect_route');
  155. $this->loginManager->loginUser(DefaultFirewall::NAME, $user, $response);
  156. $this->createDemoClassroom->create($user);
  157. return $response;
  158. }
  159. public function uniLoginAction(): Response
  160. {
  161. return $this->render('User/Security/uni_login.html.twig');
  162. }
  163. private function persistUser($user)
  164. {
  165. $this->entityManager->persist($user);
  166. $this->entityManager->flush();
  167. }
  168. private function shouldShowAlternativeLogin(?AuthenticationException $error): bool
  169. {
  170. $scope = $this->parameterBag->get('abacus.scope');
  171. if ($scope === 'gale') {
  172. // Never use UniLogin for Gale
  173. return true;
  174. }
  175. if ($this->request->query->has("alternative_login")) {
  176. return true;
  177. }
  178. // Failed alternative login attempt - keep using alternative login
  179. if ($error !== null && $this->request->getSession()->get('login_method') === 'alternative') {
  180. return true;
  181. }
  182. if ($error instanceof InvalidCsrfTokenException) {
  183. // Csrf token should only be used for alternative login
  184. return true;
  185. }
  186. // Trying to access /easyadmin and not logged in - use alternative login
  187. if ($target = $this->getTargetPath($this->request->getSession(), DefaultFirewall::NAME)) {
  188. $targetParts = parse_url($target);
  189. if (str_starts_with($targetParts['path'] ?? '', '/easyadmin')) {
  190. return true;
  191. }
  192. }
  193. return false;
  194. }
  195. }