src/Controller/SecurityController.php line 32

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\Controller;
  3. use LogicException;
  4. use MedBrief\MSR\Service\Security\IPFailedLogin;
  5. use MedBrief\MSR\Service\Security\SecurityTimeDelayHelperService;
  6. use MedBrief\MSR\Service\Security\UserFailedLogin;
  7. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\HttpFoundation\Response;
  10. use Symfony\Component\Routing\Annotation\Route;
  11. use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
  12. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  13. class SecurityController extends AbstractController
  14. {
  15. private readonly string $clientIpAddress;
  16. public function __construct(private readonly SecurityTimeDelayHelperService $securityTimeDelayHelper)
  17. {
  18. $this->clientIpAddress = Request::createFromGlobals()->getClientIp() ?? 'Unknown';
  19. }
  20. /**
  21. * @Route("/login", name="login")
  22. *
  23. * @param AuthenticationUtils $authenticationUtils
  24. * @param UserFailedLogin $userFailedLogin
  25. * @param IPFailedLogin $IPFailedLogin
  26. */
  27. public function login(AuthenticationUtils $authenticationUtils, UserFailedLogin $userFailedLogin, IPFailedLogin $IPFailedLogin): Response
  28. {
  29. // Start time to measure response duration
  30. $startTime = microtime(true);
  31. // Get the login error if there is one
  32. $error = $authenticationUtils->getLastAuthenticationError();
  33. // Last username entered by the user
  34. $lastUsername = $authenticationUtils->getLastUsername();
  35. // If the user is already logged in, redirect them to the dashboard
  36. if ($this->getUser()) {
  37. // Clear failed logins for authenticated user
  38. $userFailedLogin->loginFailureClear($this->getUser()->getUsername()); //TODO: replace ->getUsername() with ->getUserIdentifier() during next Symfony upgrade.
  39. // Clear failed logins for ip address by username
  40. $IPFailedLogin->loginFailureClear($this->getUser()->getUsername());
  41. return $this->redirectToRoute('infology_medbrief_dashboard');
  42. }
  43. // This is for email enumeration attacks
  44. // Use the SecurityHelper to determine if the user exists
  45. $userExists = $this->securityTimeDelayHelper->determineIfUserExists();
  46. if (!$userExists) {
  47. // Add an extra delay for wrong email to match password timing
  48. usleep(500 * 1000);
  49. }
  50. // Set currentCount and IPcurrentCount to 0
  51. $currentCount = 0;
  52. $IPcurrentCount = 0;
  53. // Determine if there is a login failure and Track the login failure to an IP address as well.
  54. // CSRF token errors (e.g. expired session, multiple tabs, back button) should not count
  55. // as failed login attempts since the user never submitted invalid credentials.
  56. if ($error !== null && !$error instanceof InvalidCsrfTokenException) {
  57. $IPcurrentCount = $IPFailedLogin->loginFailure($lastUsername)['currentCount'];
  58. $currentCount = $userFailedLogin->loginFailure($lastUsername)['currentCount'];
  59. }
  60. // Get the maximum attempts allowed for a failed login attempt
  61. $maxAttempts = $userFailedLogin->getMaxAttempts();
  62. // Get the maximum attempts allowed for a failed login attempt on IP Address
  63. $IPmaxAttempts = $IPFailedLogin->getMaxAttempts();
  64. // Check if the IP Address is blacklisted or exceeded the max login attempts
  65. if ($IPFailedLogin->loginAllow() === false) {
  66. // Get the login lockout time left
  67. $IPloginLockoutTimeLeft = $IPFailedLogin->getLoginLockoutTimer($lastUsername);
  68. // If there is a lockout time left, load the login lockout screen and pass relevant variables
  69. if ($IPloginLockoutTimeLeft !== null) {
  70. // Load the login lockout screen and pass relevant variables
  71. return $this->render('security/IPBlacklisted.html.twig', ['IPloginLockoutTimeLeft' => $IPloginLockoutTimeLeft, 'clientIPAddress' => $this->clientIpAddress]);
  72. }
  73. }
  74. // Check if the user or clientIP is blacklisted
  75. if ($userFailedLogin->loginAllow($lastUsername) === false) {
  76. // Get the login lockout time left
  77. $loginLockoutTimeLeft = $userFailedLogin->getLoginLockoutTimer($lastUsername);
  78. // If there is a lockout time left, load the login lockout screen and pass relevant variables
  79. if ($loginLockoutTimeLeft !== null) {
  80. // Load the login lockout screen and pass relevant variables
  81. return $this->render('security/loginLockout.html.twig', ['loginLockoutTimeLeft' => $loginLockoutTimeLeft]);
  82. }
  83. }
  84. // Use the SecurityHelper to add a random delay
  85. $this->securityTimeDelayHelper->addRandomDelay($startTime);
  86. // Load the login screen and pass relevant variables
  87. return $this->render('security/login.html.twig', [
  88. 'last_username' => $lastUsername,
  89. 'error' => $error,
  90. 'failedLoginCount' => $currentCount,
  91. 'IPfailedLoginCount' => $IPcurrentCount,
  92. 'maxAttempts' => $maxAttempts,
  93. 'IPmaxAttempts' => $IPmaxAttempts,
  94. ]);
  95. }
  96. /**
  97. * @Route("/logout", name="logout")
  98. */
  99. public function logout(): void
  100. {
  101. throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
  102. }
  103. }