<?php
namespace MedBrief\MSR\Controller;
use LogicException;
use MedBrief\MSR\Service\Security\IPFailedLogin;
use MedBrief\MSR\Service\Security\SecurityTimeDelayHelperService;
use MedBrief\MSR\Service\Security\UserFailedLogin;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
private readonly string $clientIpAddress;
public function __construct(private readonly SecurityTimeDelayHelperService $securityTimeDelayHelper)
{
$this->clientIpAddress = Request::createFromGlobals()->getClientIp() ?? 'Unknown';
}
/**
* @Route("/login", name="login")
*
* @param AuthenticationUtils $authenticationUtils
* @param UserFailedLogin $userFailedLogin
* @param IPFailedLogin $IPFailedLogin
*/
public function login(AuthenticationUtils $authenticationUtils, UserFailedLogin $userFailedLogin, IPFailedLogin $IPFailedLogin): Response
{
// Start time to measure response duration
$startTime = microtime(true);
// Get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// Last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
// If the user is already logged in, redirect them to the dashboard
if ($this->getUser()) {
// Clear failed logins for authenticated user
$userFailedLogin->loginFailureClear($this->getUser()->getUsername()); //TODO: replace ->getUsername() with ->getUserIdentifier() during next Symfony upgrade.
// Clear failed logins for ip address by username
$IPFailedLogin->loginFailureClear($this->getUser()->getUsername());
return $this->redirectToRoute('infology_medbrief_dashboard');
}
// This is for email enumeration attacks
// Use the SecurityHelper to determine if the user exists
$userExists = $this->securityTimeDelayHelper->determineIfUserExists();
if (!$userExists) {
// Add an extra delay for wrong email to match password timing
usleep(500 * 1000);
}
// Set currentCount and IPcurrentCount to 0
$currentCount = 0;
$IPcurrentCount = 0;
// Determine if there is a login failure and Track the login failure to an IP address as well
if ($error !== null) {
$IPcurrentCount = $IPFailedLogin->loginFailure($lastUsername)['currentCount'];
$currentCount = $userFailedLogin->loginFailure($lastUsername)['currentCount'];
}
// Get the maximum attempts allowed for a failed login attempt
$maxAttempts = $userFailedLogin->getMaxAttempts();
// Get the maximum attempts allowed for a failed login attempt on IP Address
$IPmaxAttempts = $IPFailedLogin->getMaxAttempts();
// Check if the IP Address is blacklisted or exceeded the max login attempts
if ($IPFailedLogin->loginAllow() === false) {
// Get the login lockout time left
$IPloginLockoutTimeLeft = $IPFailedLogin->getLoginLockoutTimer($lastUsername);
// If there is a lockout time left, load the login lockout screen and pass relevant variables
if ($IPloginLockoutTimeLeft !== null) {
// Load the login lockout screen and pass relevant variables
return $this->render('security/IPBlacklisted.html.twig', ['IPloginLockoutTimeLeft' => $IPloginLockoutTimeLeft, 'clientIPAddress' => $this->clientIpAddress]);
}
}
// Check if the user or clientIP is blacklisted
if ($userFailedLogin->loginAllow($lastUsername) === false) {
// Get the login lockout time left
$loginLockoutTimeLeft = $userFailedLogin->getLoginLockoutTimer($lastUsername);
// If there is a lockout time left, load the login lockout screen and pass relevant variables
if ($loginLockoutTimeLeft !== null) {
// Load the login lockout screen and pass relevant variables
return $this->render('security/loginLockout.html.twig', ['loginLockoutTimeLeft' => $loginLockoutTimeLeft]);
}
}
// Use the SecurityHelper to add a random delay
$this->securityTimeDelayHelper->addRandomDelay($startTime);
// Load the login screen and pass relevant variables
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
'failedLoginCount' => $currentCount,
'IPfailedLoginCount' => $IPcurrentCount,
'maxAttempts' => $maxAttempts,
'IPmaxAttempts' => $IPmaxAttempts,
]);
}
/**
* @Route("/logout", name="logout")
*/
public function logout(): void
{
throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}