Compare commits

..

No commits in common. "7032787d8c9dd8c35a85e873cc5de5b43442bdaf" and "8e5643321637623b746474091e4c84f1173e294a" have entirely different histories.

12 changed files with 179 additions and 107 deletions

View File

@ -32,9 +32,21 @@ class SecurityController extends AbstractController
$loginForm->addError(new FormError($trans->trans('error.login', [], 'messages'))); $loginForm->addError(new FormError($trans->trans('error.login', [], 'messages')));
$request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_LOGIN); $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_LOGIN);
} }
if ($request->getSession()->has(SQLLoginUserAuthenticator::TECHNICAL_ERROR)) { if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_PDO)) {
$loginForm->addError(new FormError($trans->trans('error.technical', [], 'messages'))); $loginForm->addError(new FormError($trans->trans('error.pdo', [], 'messages')));
$request->getSession()->remove(SQLLoginUserAuthenticator::TECHNICAL_ERROR); $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PDO);
}
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);
} }
} }

View File

@ -3,20 +3,14 @@
namespace App\Hydra; namespace App\Hydra;
use App\Hydra\Exception\InvalidChallengeException; use App\Hydra\Exception\InvalidChallengeException;
use Exception;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\HttpClient\ResponseInterface;
class Client class Client
{ {
private HttpClientInterface $client; protected $client;
private const MAX_RETRY = 3;
private const SLEEP_TIME = [ protected $hydraAdminBaseUrl;
5,
500,
5000,
];
private string $hydraAdminBaseUrl;
public function __construct(HttpClientInterface $client, string $hydraAdminBaseUrl) public function __construct(HttpClientInterface $client, string $hydraAdminBaseUrl)
{ {
@ -28,11 +22,11 @@ class Client
{ {
$response = $this->client->request( $response = $this->client->request(
'GET', 'GET',
$this->hydraAdminBaseUrl.'/oauth2/auth/requests/login', $this->hydraAdminBaseUrl . '/oauth2/auth/requests/login',
[ [
'query' => [ 'query' => [
'login_challenge' => $loginChallenge, 'login_challenge' => $loginChallenge,
], ]
] ]
); );
@ -41,6 +35,7 @@ class Client
throw new InvalidChallengeException(); throw new InvalidChallengeException();
} }
return $response; return $response;
} }
@ -48,11 +43,11 @@ class Client
{ {
$response = $this->client->request( $response = $this->client->request(
'GET', 'GET',
$this->hydraAdminBaseUrl.'/oauth2/auth/requests/logout', $this->hydraAdminBaseUrl . '/oauth2/auth/requests/logout',
[ [
'query' => [ 'query' => [
'logout_challenge' => $logoutChallenge, 'logout_challenge' => $logoutChallenge,
], ]
] ]
); );
@ -61,39 +56,28 @@ class Client
throw new InvalidChallengeException(); throw new InvalidChallengeException();
} }
return $response; return $response;
} }
public function fetchConsentRequestInfo(string $consentChallenge): ResponseInterface public function fetchConsentRequestInfo(string $consentChallenge): ResponseInterface
{ {
$attempt = 0; $response = $this->client->request(
while ($attempt < self::MAX_RETRY) { 'GET',
$response = $this->client->request( $this->hydraAdminBaseUrl . '/oauth2/auth/requests/consent',
'GET', [
$this->hydraAdminBaseUrl.'/oauth2/auth/requests/consent', 'query' => [
[ 'consent_challenge' => $consentChallenge,
'query' => [
'consent_challenge' => $consentChallenge,
],
] ]
); ]
);
$status = $response->getStatusCode(); switch ($response->getStatusCode()) {
if (503 === $status) { case 404:
++$attempt; throw new InvalidChallengeException();
usleep(1000 * self::SLEEP_TIME[$attempt] + rand(1, 5) * 1000);
continue;
}
switch ($status) {
case 404:
throw new InvalidChallengeException();
}
break;
}
if (self::MAX_RETRY === $attempt) {
throw new Exception(sprintf('Fetch consent a rencontré une erreur %s après %s tentatives', $response->getStatusCode(), self::MAX_RETRY));
} }
return $response; return $response;
} }
@ -101,13 +85,13 @@ class Client
{ {
$response = $this->client->request( $response = $this->client->request(
'PUT', 'PUT',
$this->hydraAdminBaseUrl.'/oauth2/auth/requests/login/accept', $this->hydraAdminBaseUrl . '/oauth2/auth/requests/login/accept',
[ [
'query' => [ 'query' => [
'login_challenge' => $loginChallenge, 'login_challenge' => $loginChallenge,
], ],
'headers' => [ 'headers' => [
'Content-Type' => 'application/json', 'Content-Type' => 'application/json'
], ],
'body' => json_encode($payload), 'body' => json_encode($payload),
] ]
@ -120,13 +104,13 @@ class Client
{ {
$response = $this->client->request( $response = $this->client->request(
'PUT', 'PUT',
$this->hydraAdminBaseUrl.'/oauth2/auth/requests/consent/accept', $this->hydraAdminBaseUrl . '/oauth2/auth/requests/consent/accept',
[ [
'query' => [ 'query' => [
'consent_challenge' => $consentChallenge, 'consent_challenge' => $consentChallenge,
], ],
'headers' => [ 'headers' => [
'Content-Type' => 'application/json', 'Content-Type' => 'application/json'
], ],
'body' => json_encode($payload), 'body' => json_encode($payload),
] ]
@ -139,13 +123,13 @@ class Client
{ {
$response = $this->client->request( $response = $this->client->request(
'PUT', 'PUT',
$this->hydraAdminBaseUrl.'/oauth2/auth/requests/logout/accept', $this->hydraAdminBaseUrl . '/oauth2/auth/requests/logout/accept',
[ [
'query' => [ 'query' => [
'logout_challenge' => $logoutChallenge, 'logout_challenge' => $logoutChallenge,
], ],
'headers' => [ 'headers' => [
'Content-Type' => 'application/json', 'Content-Type' => 'application/json'
], ],
] ]
); );

View File

@ -4,6 +4,6 @@ namespace App\SQLLogin\Exception;
use Exception; use Exception;
class EmptyResultException extends Exception class DatabaseConnectionException extends Exception
{ {
} }

View File

@ -0,0 +1,9 @@
<?php
namespace App\SQLLogin\Exception;
use Exception;
class InvalidSQLLoginAlgoException extends Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\SQLLogin\Exception;
use Exception;
class LoginElementsConfigurationException extends Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\SQLLogin\Exception;
use Exception;
class NullPasswordColumnNameException extends Exception
{
}

View File

@ -24,6 +24,12 @@ class SQLLoginConnect extends AbstractController
public function connect($urlDatabase, $dbUser, $dbPassword): PDO public function connect($urlDatabase, $dbUser, $dbPassword): PDO
{ {
return new PDO($urlDatabase, $dbUser, $dbPassword); $options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
PDO::ATTR_PERSISTENT => false,
];
return new PDO($urlDatabase, $dbUser, $dbPassword, $options);
} }
} }

View File

@ -88,7 +88,7 @@ class PasswordEncoder implements LegacyPasswordHasherInterface
foreach ($this->securityPattern as $term) { foreach ($this->securityPattern as $term) {
if (self::PEPPER_PATTERN !== $term && self::PASSWORD_PATTERN !== $term && self::SALT_PATTERN !== $term) { if (self::PEPPER_PATTERN !== $term && self::PASSWORD_PATTERN !== $term && self::SALT_PATTERN !== $term) {
$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); $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('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 = ''; $completedPlainPassword = '';

View File

@ -5,12 +5,12 @@ namespace App\Security;
use App\Entity\User; use App\Entity\User;
use App\Security\Hasher\PasswordEncoder; use App\Security\Hasher\PasswordEncoder;
use App\Service\SQLLoginService; use App\Service\SQLLoginService;
use App\SQLLogin\Exception\DatabaseConnectionException;
use App\SQLLogin\Exception\DataToFetchConfigurationException; use App\SQLLogin\Exception\DataToFetchConfigurationException;
use App\SQLLogin\Exception\EmptyResultException;
use App\SQLLogin\Exception\InvalidSQLPasswordException; use App\SQLLogin\Exception\InvalidSQLPasswordException;
use App\SQLLogin\Exception\LoginElementsConfigurationException;
use App\SQLLogin\Exception\SecurityPatternConfigurationException; use App\SQLLogin\Exception\SecurityPatternConfigurationException;
use App\SQLLogin\SQLLoginRequest; use App\SQLLogin\SQLLoginRequest;
use PDOException;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@ -25,7 +25,11 @@ class SQLLoginUserAuthenticator extends AbstractLoginFormAuthenticator
{ {
public const LOGIN_ROUTE = 'app_login'; public const LOGIN_ROUTE = 'app_login';
public const ERROR_LOGIN = 'error_login'; public const ERROR_LOGIN = 'error_login';
public const TECHNICAL_ERROR = 'technical_error'; 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';
private string $baseUrl; private string $baseUrl;
@ -72,46 +76,48 @@ class SQLLoginUserAuthenticator extends AbstractLoginFormAuthenticator
$session = $request->getSession(); $session = $request->getSession();
try { try {
$datas = $this->sqlLoginService->fetchPasswordAndDatas($login); $datas = $this->sqlLoginService->fetchPasswordAndDatas($login);
} catch (EmptyResultException $e) { $remoteHashedPassword = $datas[$this->sqlLoginRequest->getPasswordColumnName()];
$session->set(self::ERROR_LOGIN, true); unset($datas[$this->sqlLoginRequest->getPasswordColumnName()]);
$remoteSalt = null;
if ($this->sqlLoginRequest->getSaltColumnName() && isset($datas[$this->sqlLoginRequest->getSaltColumnName()])) {
$remoteSalt = $datas[$this->sqlLoginRequest->getSaltColumnName()];
unset($datas[$this->sqlLoginRequest->getSaltColumnName()]);
}
} catch (DatabaseConnectionException $e) {
$session->set(self::ERROR_PDO, true);
throw new AuthenticationException(); throw new AuthenticationException();
} catch (DataToFetchConfigurationException|PDOException $e) { } catch (LoginElementsConfigurationException $e) {
\Sentry\captureException($e); $session->set(self::ERROR_CONFIGURATION, true);
$session->set(self::TECHNICAL_ERROR, true); throw new AuthenticationException();
} catch (DataToFetchConfigurationException $e) {
$session->set(self::ERROR_DATA_TO_FETCH_CONFIGURATION, true);
throw new AuthenticationException(); throw new AuthenticationException();
} }
$remoteHashedPassword = $datas[$this->sqlLoginRequest->getPasswordColumnName()];
unset($datas[$this->sqlLoginRequest->getPasswordColumnName()]);
$remoteSalt = null;
if ($this->sqlLoginRequest->getSaltColumnName() && isset($datas[$this->sqlLoginRequest->getSaltColumnName()])) {
$remoteSalt = $datas[$this->sqlLoginRequest->getSaltColumnName()];
unset($datas[$this->sqlLoginRequest->getSaltColumnName()]);
}
if (null === $remoteHashedPassword) { if (null === $remoteHashedPassword) {
$remoteHashedPassword = ''; $remoteHashedPassword = '';
} }
try { try {
// Comparaison remote hash et hash du input password + salt // Comparaison remote hash et hash du input password + salt
$this->passwordHasher->verify($remoteHashedPassword, $plaintextPassword, $remoteSalt); $this->passwordHasher->verify($remoteHashedPassword, $plaintextPassword, $remoteSalt);
$user = new User($login, $remoteHashedPassword, $datas, $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) { } catch (InvalidSQLPasswordException $e) {
$session->set(self::ERROR_LOGIN, true); $session->set(self::ERROR_LOGIN, true);
throw new AuthenticationException(); throw new AuthenticationException();
} catch (SecurityPatternConfigurationException $e) { } catch (SecurityPatternConfigurationException $e) {
\Sentry\captureException($e); $session->set(self::ERROR_SECURITY_PATTERN_CONFIGURATION, true);
$session->set(self::TECHNICAL_ERROR, true);
throw new AuthenticationException(); throw new AuthenticationException();
} }
$user = new User($login, $remoteHashedPassword, $datas, $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;
} }
protected function getLoginUrl(Request $request): string protected function getLoginUrl(Request $request): string

View File

@ -2,16 +2,20 @@
namespace App\Service; namespace App\Service;
use App\SQLLogin\Exception\EmptyResultException; 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\SQLLoginConnect;
use App\SQLLogin\SQLLoginRequest; use App\SQLLogin\SQLLoginRequest;
use PDO; use PDO;
use PDOException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class SQLLoginService extends AbstractController class SQLLoginService extends AbstractController
{ {
public const MAX_RETRY = 3; private PDO $pdo;
public function __construct( public function __construct(
private SQLLoginRequest $sqlLoginRequest, private SQLLoginRequest $sqlLoginRequest,
@ -19,35 +23,31 @@ class SQLLoginService extends AbstractController
) { ) {
$this->sqlLoginRequest = $sqlLoginRequest; $this->sqlLoginRequest = $sqlLoginRequest;
$this->loggerInterface = $loggerInterface; $this->loggerInterface = $loggerInterface;
$this->pdo = $this->getConnection();
} }
public function fetchPasswordAndDatas(string $login): array public function fetchPasswordAndDatas(string $login): array
{ {
$dataRequest = $this->sqlLoginRequest->getDatasRequest(); try {
$datas = $this->executeRequestWithLogin($dataRequest, $login); $dataRequest = $this->sqlLoginRequest->getDatasRequest();
$datas = $this->executeRequestWithLogin($dataRequest, $login);
} catch (NullDataToFetchException $e) {
throw new DataToFetchConfigurationException($e->getMessage());
} catch (PDOException $e) {
$this->loggerInterface->critical($e->getMessage());
throw new LoginElementsConfigurationException($e->getMessage());
}
return $datas; return $datas;
} }
private function executeRequestWithLogin(string $request, string $login): array private function executeRequestWithLogin(string $request, string $login): array
{ {
$attempt = 0; $query = $this->pdo->prepare($request);
while ($attempt < self::MAX_RETRY) { $query->bindParam($this->sqlLoginRequest->getLoginColumnName(), $login, PDO::PARAM_STR);
$pdo = $this->getConnection(); $query->execute();
$query = $pdo->prepare($request); $datas = $query->fetch(PDO::FETCH_ASSOC);
$query->execute([$this->sqlLoginRequest->getLoginColumnName() => $login]); $query->closeCursor();
$datas = $query->fetch(PDO::FETCH_ASSOC);
$query->closeCursor();
if (false === $datas) {
usleep(3000);
++$attempt;
} else {
break;
}
}
if (self::MAX_RETRY === $attempt) {
throw new EmptyResultException();
}
return $datas; return $datas;
} }
@ -55,8 +55,13 @@ class SQLLoginService extends AbstractController
private function getConnection(): PDO private function getConnection(): PDO
{ {
// Appel du singleton // Appel du singleton
$sqlLogin = SQLLoginConnect::getInstance(); try {
$pdo = $sqlLogin->connect($this->sqlLoginRequest->getDatabaseDsn(), $this->sqlLoginRequest->getDbUser(), $this->sqlLoginRequest->getDbPassword()); $sqlLogin = SQLLoginConnect::getInstance();
$pdo = $sqlLogin->connect($this->sqlLoginRequest->getDatabaseDsn(), $this->sqlLoginRequest->getDbUser(), $this->sqlLoginRequest->getDbPassword());
} catch (PDOException $e) {
$this->loggerInterface->critical($e->getMessage());
throw new DatabaseConnectionException($e->getMessage());
}
return $pdo; return $pdo;
} }

View File

@ -9,9 +9,25 @@
<source>error.login</source> <source>error.login</source>
<target>Incorrect login or password</target> <target>Incorrect login or password</target>
</trans-unit> </trans-unit>
<trans-unit id="1QRR4uA" resname="error.technical"> <trans-unit id="36t19qm" resname="error.sql_login">
<source>error.technical</source> <source>error.sql_login</source>
<target>A technical error happened, try again later</target> <target>Connection to database encountered a problem</target>
</trans-unit>
<trans-unit id="lBole_G" resname="error.pdo">
<source>error.pdo</source>
<target>Connection to database encountered a problem</target>
</trans-unit>
<trans-unit id="1QRR4uA" resname="error.configuration">
<source>error.configuration</source>
<target>Identification data references do not exist in the database</target>
</trans-unit>
<trans-unit id="4EPIhsV" resname="error.data_to_fetch_configuration">
<source>error.data_to_fetch_configuration</source>
<target>Data references to be transmitted do not exist</target>
</trans-unit>
<trans-unit id="6iuTNs3" resname="error.security_pattern_configuration">
<source>error.security_pattern_configuration</source>
<target>The security pattern is not allowed</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9,9 +9,25 @@
<source>error.login</source> <source>error.login</source>
<target>Login ou mot de passe inconnu</target> <target>Login ou mot de passe inconnu</target>
</trans-unit> </trans-unit>
<trans-unit id="1QRR4uA" resname="error.technical"> <trans-unit id="36t19qm" resname="error.sql_login">
<source>error.technical</source> <source>error.sql_login</source>
<target>Une erreur technique s'est produite, rééssayez plus tard</target> <target>La connexion à la base de données a rencontré un problème</target>
</trans-unit>
<trans-unit id="lBole_G" resname="error.pdo">
<source>error.pdo</source>
<target>La connexion à la base de données a rencontré un problème</target>
</trans-unit>
<trans-unit id="1QRR4uA" resname="error.configuration">
<source>error.configuration</source>
<target>Les références de données d'identification n'existent pas dans la base de données</target>
</trans-unit>
<trans-unit id="4EPIhsV" resname="error.data_to_fetch_configuration">
<source>error.data_to_fetch_configuration</source>
<target>Les références de données à transmettre n'existent pas</target>
</trans-unit>
<trans-unit id="6iuTNs3" resname="error.security_pattern_configuration">
<source>error.security_pattern_configuration</source>
<target>Le patron de sécurité n'est pas autorisé</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>