diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 08a0c76..71fd3de 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -17,7 +17,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; class SecurityController extends AbstractController { #[Route('/login', name: 'app_login')] - public function login(ParameterBagInterface $params, AuthenticationUtils $authenticationUtils, Request $request, TranslatorInterface $trans): Response + public function login(ParameterBagInterface $params, AuthenticationUtils $authenticationUtils, Request $request, TranslatorInterface $trans): Response|RedirectResponse { // Si l'utilisateur est déjà connecté on le renvoie sur la page du site demandeur if ($this->getUser()) { @@ -29,16 +29,24 @@ class SecurityController extends AbstractController $error = $authenticationUtils->getLastAuthenticationError(); if ($error) { if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_LOGIN)) { - $loginForm->get('login')->addError(new FormError($trans->trans('error.login', [], 'messages'))); + $loginForm->addError(new FormError($trans->trans('error.login', [], 'messages'))); $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_LOGIN); } - if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_PASSWORD)) { - $loginForm->get('password')->addError(new FormError($trans->trans('error.password', [], 'messages'))); - $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PASSWORD); + if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_PDO)) { + $loginForm->addError(new FormError($trans->trans('error.pdo', [], 'messages'))); + $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PDO); } - if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_SQL_LOGIN)) { - $loginForm->addError(new FormError($trans->trans('error.sql_login', [], 'messages'))); - $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_SQL_LOGIN); + if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_CONFIGURATION)) { + $loginForm->addError(new FormError($trans->trans('error.configuration', [], 'messages'))); + $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_CONFIGURATION); + } + if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_DATA_TO_FETCH_CONFIGURATION)) { + $loginForm->addError(new FormError($trans->trans('error.data_to_fetch_configuration', [], 'messages'))); + $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_DATA_TO_FETCH_CONFIGURATION); + } + if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_SECURITY_PATTERN_CONFIGURATION)) { + $loginForm->addError(new FormError($trans->trans('error.security_pattern_configuration', [], 'messages'))); + $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_SECURITY_PATTERN_CONFIGURATION); } } diff --git a/src/SQLLogin/Exception/InvalidSQLLoginConfigurationException.php b/src/SQLLogin/Exception/DataToFetchConfigurationException.php similarity index 50% rename from src/SQLLogin/Exception/InvalidSQLLoginConfigurationException.php rename to src/SQLLogin/Exception/DataToFetchConfigurationException.php index 156abf7..7b09626 100644 --- a/src/SQLLogin/Exception/InvalidSQLLoginConfigurationException.php +++ b/src/SQLLogin/Exception/DataToFetchConfigurationException.php @@ -4,6 +4,6 @@ namespace App\SQLLogin\Exception; use Exception; -class InvalidSQLLoginConfigurationException extends Exception +class DataToFetchConfigurationException extends Exception { } diff --git a/src/SQLLogin/Exception/DatabaseConnectionException.php b/src/SQLLogin/Exception/DatabaseConnectionException.php new file mode 100644 index 0000000..770ea6b --- /dev/null +++ b/src/SQLLogin/Exception/DatabaseConnectionException.php @@ -0,0 +1,9 @@ +config[self::DATA_TO_FETCH] as $data) { - $scope .= $data.','; + if (!$this->config[self::DATA_TO_FETCH]) { + throw new NullDataToFetchException(); } + + foreach ($this->config[self::DATA_TO_FETCH] as $data) { + $scope .= $data . ','; + } + // On enlève la dernière virgule $scope = substr($scope, 0, -1); - return 'SELECT '.$scope.' FROM '.$this->getTableName().' WHERE '.$this->getLoginColumnName().' = :'.$this->getLoginColumnName().';'; + return 'SELECT ' . $scope . ' FROM ' . $this->getTableName() . ' WHERE ' . $this->getLoginColumnName() . ' = :' . $this->getLoginColumnName() . ';'; } /** * Construction de la string pour la requête préparée selon la configuration yaml * intègre la récupération du mot de passe hashé, du salt et de besoin d'upgrade de la méthode de hashage */ - public function getRequestPassword() + public function getRequestPassword(): string { $fields = $this->getPasswordColumnName(); if (!empty($this->getSaltColumnName())) { - $fields .= ', '.$this->getSaltColumnName(); + $fields .= ', ' . $this->getSaltColumnName(); } - return 'SELECT '.$fields.' FROM '.$this->getTableName().' WHERE '.$this->getLoginColumnName().' = :'.$this->getLoginColumnName().';'; + return 'SELECT ' . $fields . ' FROM ' . $this->getTableName() . ' WHERE ' . $this->getLoginColumnName() . ' = :' . $this->getLoginColumnName() . ';'; } } diff --git a/src/Security/Hasher/PasswordEncoder.php b/src/Security/Hasher/PasswordEncoder.php index c2d725a..91ffa7b 100644 --- a/src/Security/Hasher/PasswordEncoder.php +++ b/src/Security/Hasher/PasswordEncoder.php @@ -2,8 +2,9 @@ namespace App\Security\Hasher; -use App\SQLLogin\Exception\InvalidSQLLoginConfigurationException; use App\SQLLogin\Exception\InvalidSQLPasswordException; +use App\SQLLogin\Exception\SecurityPatternConfigurationException; +use Psr\Log\LoggerInterface; use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait; use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface; @@ -19,7 +20,7 @@ class PasswordEncoder implements LegacyPasswordHasherInterface protected array $hashAlgoLegacy; protected array $securityPattern; - public function __construct(?string $pepper, string $hashAlgoLegacy, string $securityPattern) + public function __construct(?string $pepper, string $hashAlgoLegacy, string $securityPattern, private LoggerInterface $loggerInterface) { $this->pepper = $pepper; $this->hashAlgoLegacy = explode(',', $hashAlgoLegacy); @@ -88,7 +89,8 @@ class PasswordEncoder implements LegacyPasswordHasherInterface foreach ($this->securityPattern as $term) { if (self::PEPPER_PATTERN !== $term && self::PASSWORD_PATTERN !== $term && self::SALT_PATTERN !== $term) { - throw new InvalidSQLLoginConfigurationException(); + $this->loggerInterface->critical('La configuration du security pattern est invalide, les termes autorisés sont : '.self::PASSWORD_PATTERN.', '.self::SALT_PATTERN.' et '.self::PEPPER_PATTERN); + throw new SecurityPatternConfigurationException(); } } $completedPlainPassword = ''; diff --git a/src/Security/SQLLoginUserAuthenticator.php b/src/Security/SQLLoginUserAuthenticator.php index 1d3394e..6670214 100644 --- a/src/Security/SQLLoginUserAuthenticator.php +++ b/src/Security/SQLLoginUserAuthenticator.php @@ -5,26 +5,31 @@ namespace App\Security; use App\Entity\User; use App\Security\Hasher\PasswordEncoder; use App\Service\SQLLoginService; +use App\SQLLogin\Exception\DatabaseConnectionException; +use App\SQLLogin\Exception\DataToFetchConfigurationException; use App\SQLLogin\Exception\InvalidSQLPasswordException; -use PDOException; +use App\SQLLogin\Exception\LoginElementsConfigurationException; +use App\SQLLogin\Exception\SecurityPatternConfigurationException; +use Exception; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; -use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; class SQLLoginUserAuthenticator extends AbstractLoginFormAuthenticator { public const LOGIN_ROUTE = 'app_login'; public const ERROR_LOGIN = 'error_login'; - public const ERROR_PASSWORD = 'error_password'; + public const ERROR_PDO = 'error_pdo'; public const ERROR_SQL_LOGIN = 'error_sql_login'; + public const ERROR_CONFIGURATION = 'error_configuration'; + public const ERROR_DATA_TO_FETCH_CONFIGURATION = 'error_data_to_fetch_configuration'; + public const ERROR_SECURITY_PATTERN_CONFIGURATION = 'error_security_pattern_configuration'; protected string $baseUrl; private SQLLoginService $sqlLoginService; @@ -47,62 +52,74 @@ class SQLLoginUserAuthenticator extends AbstractLoginFormAuthenticator return self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST'); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): RedirectResponse { - return new RedirectResponse($this->baseUrl.'/connect/login-accept'); + return new RedirectResponse($this->baseUrl . '/connect/login-accept'); } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): RedirectResponse { $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); - return new RedirectResponse($this->baseUrl.'/login'); + return new RedirectResponse($this->baseUrl . '/login'); } - public function authenticate(Request $request): Passport + public function authenticate(Request $request): SelfValidatingPassport { $form = $request->request->get('login'); $login = $form['login']; $plaintextPassword = $form['password']; $rememberMe = isset($form['_remember_me']) ? true : false; + $session = $request->getSession(); try { // requête préparée list($remoteHashedPassword, $remoteSalt) = $this->sqlLoginService->fetchPassword($login); - } catch (PDOException $e) { - $request->getSession()->set(self::ERROR_SQL_LOGIN, true); + } catch (DatabaseConnectionException $e) { + $session->set(self::ERROR_PDO, true); + throw new AuthenticationException(); + } catch (LoginElementsConfigurationException $e) { + $session->set(self::ERROR_CONFIGURATION, true); + throw new AuthenticationException(); + } catch (Exception $exception) { + $request->getSession()->set(self::ERROR_LOGIN, true); throw new AuthenticationException(); } - if ($remoteHashedPassword) { - try { - // Comparaison remote hash et hash du input password + salt - $this->passwordHasher->verify($remoteHashedPassword, $plaintextPassword, $remoteSalt); - $attributes = $this->sqlLoginService->fetchDatas($login); - $user = new User($login, $remoteHashedPassword, $attributes, $rememberMe); - - $loader = function (string $userIdentifier) use ($user) { - return $user->getLogin() == $userIdentifier ? $user : null; - }; - $passport = new SelfValidatingPassport(new UserBadge($login, $loader)); - if ($rememberMe) { - $passport->addBadge(new RememberMeBadge()); - } - $passport->setAttribute('attributes', $user->getAttributes()); - - return $passport; - } catch (InvalidSQLPasswordException $e) { - $request->getSession()->set(self::ERROR_PASSWORD, true); - throw new AuthenticationException(); - } catch (PDOException $e) { - $request->getSession()->set(self::ERROR_SQL_LOGIN, true); - throw new AuthenticationException(); - } + if (!$remoteHashedPassword) { + throw new Exception('Erreur inconnue'); + } + try { + // Comparaison remote hash et hash du input password + salt + $this->passwordHasher->verify($remoteHashedPassword, $plaintextPassword, $remoteSalt); + $attributes = $this->sqlLoginService->fetchDatas($login); + $user = new User($login, $remoteHashedPassword, $attributes, $rememberMe); + + $loader = function (string $userIdentifier) use ($user) { + return $user->getLogin() == $userIdentifier ? $user : null; + }; + $passport = new SelfValidatingPassport(new UserBadge($login, $loader)); + if ($rememberMe) { + $passport->addBadge(new RememberMeBadge()); + } + $passport->setAttribute('attributes', $user->getAttributes()); + + return $passport; + } catch (InvalidSQLPasswordException $e) { + $session->set(self::ERROR_LOGIN, true); + throw new AuthenticationException(); + } catch (DataToFetchConfigurationException $e) { + $session->set(self::ERROR_DATA_TO_FETCH_CONFIGURATION, true); + throw new AuthenticationException(); + } catch (DatabaseConnectionException $e) { + $session->set(self::ERROR_PDO, true); + throw new AuthenticationException(); + } catch (SecurityPatternConfigurationException $e) { + $session->set(self::ERROR_SECURITY_PATTERN_CONFIGURATION, true); + throw new AuthenticationException(); } - $request->getSession()->set(self::ERROR_LOGIN, true); - throw new AuthenticationException(); } protected function getLoginUrl(Request $request): string { - return $this->baseUrl.'/login'; + return $this->baseUrl . '/login'; } } diff --git a/src/Service/SQLLoginService.php b/src/Service/SQLLoginService.php index 16b3d45..2011d60 100644 --- a/src/Service/SQLLoginService.php +++ b/src/Service/SQLLoginService.php @@ -2,63 +2,87 @@ namespace App\Service; +use App\SQLLogin\Exception\DatabaseConnectionException; +use App\SQLLogin\Exception\DataToFetchConfigurationException; +use App\SQLLogin\Exception\LoginElementsConfigurationException; +use App\SQLLogin\Exception\NullDataToFetchException; use App\SQLLogin\SQLLoginConnect; use App\SQLLogin\SQLLoginRequest; +use Exception; use PDO; use PDOException; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; class SQLLoginService extends AbstractController { public SQLLoginRequest $sqlLoginRequest; - public function __construct(SQLLoginRequest $sqlLoginRequest) + public function __construct(SQLLoginRequest $sqlLoginRequest, private LoggerInterface $loggerInterface) { $this->sqlLoginRequest = $sqlLoginRequest; + $this->loggerInterface = $loggerInterface; } - public function fetchDatas(string $login) + public function fetchDatas(string $login): array { try { $dbh = $this->getConnection(); + } catch (PDOException $e) { + $this->loggerInterface->critical($e->getMessage()); + throw new DatabaseConnectionException($e->getMessage()); + } + try { // forge de la requête $request = $this->sqlLoginRequest->getRequestScope(); + } catch (NullDataToFetchException $e) { + throw new DataToFetchConfigurationException($e->getMessage()); + } + + try { // Préparation de la requête $query = $dbh->prepare($request); $query->execute([$this->sqlLoginRequest->getLoginColumnName() => $login]); $datas = $query->fetch(PDO::FETCH_ASSOC); } catch (PDOException $e) { - \Sentry\captureException($e); - - throw new PDOException(); + $this->loggerInterface->critical($e->getMessage()); + throw new DataToFetchConfigurationException($e->getMessage()); } return $datas; } - public function fetchPassword(string $login) + public function fetchPassword(string $login): array { try { $dbh = $this->getConnection(); - $request = $this->sqlLoginRequest->getRequestPassword(); + } catch (PDOException $e) { + $this->loggerInterface->critical($e->getMessage()); + throw new DatabaseConnectionException($e->getMessage()); + } + + // forge de la requête + $request = $this->sqlLoginRequest->getRequestPassword(); + + try { $query = $dbh->prepare($request); $query->execute([$this->sqlLoginRequest->getLoginColumnName() => $login]); $password = $query->fetch(PDO::FETCH_ASSOC); } catch (PDOException $e) { - \Sentry\captureException($e); - throw new PDOException(); + $this->loggerInterface->critical($e->getMessage()); + throw new LoginElementsConfigurationException($e->getMessage()); } - if ($password) { - return [ - $password[$this->sqlLoginRequest->getPasswordColumnName()], - isset($password[$this->sqlLoginRequest->getSaltColumnName()]) ? $password[$this->sqlLoginRequest->getSaltColumnName()] : null, - ]; + if (!$password) { + throw new Exception('Une erreur est survenue lors de la récupération des données'); } - - return false; + + return [ + $password[$this->sqlLoginRequest->getPasswordColumnName()], + isset($password[$this->sqlLoginRequest->getSaltColumnName()]) ? $password[$this->sqlLoginRequest->getSaltColumnName()] : null, + ]; } - public function getConnection() + public function getConnection(): PDO { // Appel du singleton $sqlLogin = SQLLoginConnect::getInstance(); diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index f04251d..8617499 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -7,11 +7,7 @@