8 Commits

Author SHA1 Message Date
f007dcf6d8 feat: add authentication success/failure log outputs
Some checks failed
Cadoles/hydra-sql/pipeline/pr-develop There was a failure building this commit
see CNOUS/mse#4707
2025-03-07 09:11:25 +01:00
075be9b0df Merge pull request 'recherche login lower dans requête de connexion' (#48) from issue-47 into develop
Some checks reported warnings
Cadoles/hydra-sql/pipeline/head This commit is unstable
Reviewed-on: #48
2024-11-06 11:13:10 +01:00
4e4c5d8e7b recherche login lower dans requête de connexion
Some checks failed
Cadoles/hydra-sql/pipeline/pr-develop There was a failure building this commit
Cadoles/hydra-sql/pipeline/head This commit is unstable
2024-11-06 10:22:48 +01:00
7032787d8c Merge pull request 'fix login sql : ajout d'un retry sur le login, suppression des options' (#45) from retour-43 into develop
Some checks reported warnings
Cadoles/hydra-sql/pipeline/head This commit was not built
Reviewed-on: #45
2024-10-14 10:40:12 +02:00
999e708ff7 modification gestion des exceptions
Some checks failed
Cadoles/hydra-sql/pipeline/pr-develop There was a failure building this commit
2024-10-11 15:09:52 +02:00
cb8361e7d1 optimisation appel pdo, retry consent
Some checks failed
Cadoles/hydra-sql/pipeline/pr-develop There was a failure building this commit
2024-10-11 13:25:21 +02:00
e3f406a8bb fix resultat null vaut login inconnu
Some checks failed
Cadoles/hydra-sql/pipeline/pr-develop There was a failure building this commit
2024-10-10 16:32:46 +02:00
d6d9e81df6 fix login sql : ajout d'un retry sur le login, suppression des options
Some checks failed
Cadoles/hydra-sql/pipeline/head This commit is unstable
Cadoles/hydra-sql/pipeline/pr-develop There was a failure building this commit
2024-10-10 12:01:15 +02:00
21 changed files with 415 additions and 186 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
lts/iron

View File

@ -20,6 +20,7 @@
"symfony/form": "5.4.*", "symfony/form": "5.4.*",
"symfony/framework-bundle": "5.4.*", "symfony/framework-bundle": "5.4.*",
"symfony/http-client": "5.4.*", "symfony/http-client": "5.4.*",
"symfony/monolog-bundle": "^3.10",
"symfony/rate-limiter": "5.4.*", "symfony/rate-limiter": "5.4.*",
"symfony/runtime": "5.4.*", "symfony/runtime": "5.4.*",
"symfony/security-bundle": "5.4.*", "symfony/security-bundle": "5.4.*",

269
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "203398b3a4f3ff689ff3341c02460f23", "content-hash": "9daf21762ed80ef11e74a53f5d27119f",
"packages": [ "packages": [
{ {
"name": "clue/stream-filter", "name": "clue/stream-filter",
@ -589,6 +589,108 @@
}, },
"time": "2024-03-08T09:58:59+00:00" "time": "2024-03-08T09:58:59+00:00"
}, },
{
"name": "monolog/monolog",
"version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "5cf826f2991858b54d5c3809bee745560a1042a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7",
"reference": "5cf826f2991858b54d5c3809bee745560a1042a7",
"shasum": ""
},
"require": {
"php": ">=7.2",
"psr/log": "^1.0.1 || ^2.0 || ^3.0"
},
"provide": {
"psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^7 || ^8",
"ext-json": "*",
"graylog2/gelf-php": "^1.4.2 || ^2@dev",
"guzzlehttp/guzzle": "^7.4",
"guzzlehttp/psr7": "^2.2",
"mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4 || ^3",
"phpspec/prophecy": "^1.15",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^8.5.38 || ^9.6.19",
"predis/predis": "^1.1 || ^2.0",
"rollbar/rollbar": "^1.3 || ^2 || ^3",
"ruflin/elastica": "^7",
"swiftmailer/swiftmailer": "^5.3|^6.0",
"symfony/mailer": "^5.4 || ^6",
"symfony/mime": "^5.4 || ^6"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
"ext-mbstring": "Allow to work properly with unicode symbols",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"ext-openssl": "Required to send log messages using SSL",
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "https://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "https://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/2.10.0"
},
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
"type": "tidelift"
}
],
"time": "2024-11-12T12:43:37+00:00"
},
{ {
"name": "php-http/client-common", "name": "php-http/client-common",
"version": "2.7.2", "version": "2.7.2",
@ -3566,6 +3668,171 @@
], ],
"time": "2024-05-31T14:33:22+00:00" "time": "2024-05-31T14:33:22+00:00"
}, },
{
"name": "symfony/monolog-bridge",
"version": "v5.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bridge.git",
"reference": "cf7d75d4d64a41fbb1c0e92301bec404134fa84b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/cf7d75d4d64a41fbb1c0e92301bec404134fa84b",
"reference": "cf7d75d4d64a41fbb1c0e92301bec404134fa84b",
"shasum": ""
},
"require": {
"monolog/monolog": "^1.25.1|^2",
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/http-kernel": "^5.3|^6.0",
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.1|^2|^3"
},
"conflict": {
"symfony/console": "<4.4",
"symfony/http-foundation": "<5.3"
},
"require-dev": {
"symfony/console": "^4.4|^5.0|^6.0",
"symfony/http-client": "^4.4|^5.0|^6.0",
"symfony/mailer": "^4.4|^5.0|^6.0",
"symfony/messenger": "^4.4|^5.0|^6.0",
"symfony/mime": "^4.4|^5.0|^6.0",
"symfony/security-core": "^4.4|^5.0|^6.0",
"symfony/var-dumper": "^4.4|^5.0|^6.0"
},
"suggest": {
"symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings.",
"symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.",
"symfony/var-dumper": "For using the debugging handlers like the console handler or the log server handler."
},
"type": "symfony-bridge",
"autoload": {
"psr-4": {
"Symfony\\Bridge\\Monolog\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides integration for Monolog with various Symfony components",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/monolog-bridge/tree/v5.4.45"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-10-10T06:37:45+00:00"
},
{
"name": "symfony/monolog-bundle",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bundle.git",
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
"shasum": ""
},
"require": {
"monolog/monolog": "^1.25.1 || ^2.0 || ^3.0",
"php": ">=7.2.5",
"symfony/config": "^5.4 || ^6.0 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
"symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^6.3 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Bundle\\MonologBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony MonologBundle",
"homepage": "https://symfony.com",
"keywords": [
"log",
"logging"
],
"support": {
"issues": "https://github.com/symfony/monolog-bundle/issues",
"source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-11-06T17:08:13+00:00"
},
{ {
"name": "symfony/options-resolver", "name": "symfony/options-resolver",
"version": "v5.4.40", "version": "v5.4.40",

View File

@ -8,4 +8,5 @@ return [
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Sentry\SentryBundle\SentryBundle::class => ['all' => true], Sentry\SentryBundle\SentryBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
]; ];

View File

@ -0,0 +1,6 @@
monolog:
handlers:
stdout:
type: stream
path: "php://stdout"
level: "%env(string:LOGGER_LEVEL)%"

View File

@ -29,6 +29,9 @@ parameters:
env(PEPPER): ~ env(PEPPER): ~
pepper: '%env(resolve:PEPPER)%' pepper: '%env(resolve:PEPPER)%'
env(LOGGER_LEVEL): "info"
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
_defaults: _defaults:

View File

@ -13,7 +13,7 @@ services:
ports: ports:
- 8082:8071 - 8082:8071
volumes: volumes:
- .:/app - .:/app
tmpfs: tmpfs:
- /var/www/var/logs:uid=${FIXUID:-1000},gid=${FIXGID:-1000} - /var/www/var/logs:uid=${FIXUID:-1000},gid=${FIXGID:-1000}
- /var/www/var/cache:uid=${FIXUID:-1000},gid=${FIXGID:-1000} - /var/www/var/cache:uid=${FIXUID:-1000},gid=${FIXGID:-1000}
@ -40,6 +40,9 @@ services:
- HASH_ALGO_LEGACY="sha256" - HASH_ALGO_LEGACY="sha256"
- SECURITY_PATTERN=password,salt,pepper - SECURITY_PATTERN=password,salt,pepper
- CADDY_HTTP_PORT=8071 - CADDY_HTTP_PORT=8071
- LOGGER_LEVEL=info
- PHP_FPM_DISPLAY_ERRORS=on
- PHP_FPM_CATCH_WORKERS_OUTPUT=1
oidc-test: oidc-test:
image: bornholm/oidc-test:v0.0.0-1-g936a77e image: bornholm/oidc-test:v0.0.0-1-g936a77e

View File

@ -32,21 +32,9 @@ 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::ERROR_PDO)) { if ($request->getSession()->has(SQLLoginUserAuthenticator::TECHNICAL_ERROR)) {
$loginForm->addError(new FormError($trans->trans('error.pdo', [], 'messages'))); $loginForm->addError(new FormError($trans->trans('error.technical', [], 'messages')));
$request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PDO); $request->getSession()->remove(SQLLoginUserAuthenticator::TECHNICAL_ERROR);
}
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,14 +3,20 @@
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
{ {
protected $client; private HttpClientInterface $client;
private const MAX_RETRY = 3;
protected $hydraAdminBaseUrl; private const SLEEP_TIME = [
5,
500,
5000,
];
private string $hydraAdminBaseUrl;
public function __construct(HttpClientInterface $client, string $hydraAdminBaseUrl) public function __construct(HttpClientInterface $client, string $hydraAdminBaseUrl)
{ {
@ -22,11 +28,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,
] ],
] ]
); );
@ -35,7 +41,6 @@ class Client
throw new InvalidChallengeException(); throw new InvalidChallengeException();
} }
return $response; return $response;
} }
@ -43,11 +48,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,
] ],
] ]
); );
@ -56,27 +61,38 @@ class Client
throw new InvalidChallengeException(); throw new InvalidChallengeException();
} }
return $response; return $response;
} }
public function fetchConsentRequestInfo(string $consentChallenge): ResponseInterface public function fetchConsentRequestInfo(string $consentChallenge): ResponseInterface
{ {
$response = $this->client->request( $attempt = 0;
'GET', while ($attempt < self::MAX_RETRY) {
$this->hydraAdminBaseUrl . '/oauth2/auth/requests/consent', $response = $this->client->request(
[ 'GET',
'query' => [ $this->hydraAdminBaseUrl.'/oauth2/auth/requests/consent',
'consent_challenge' => $consentChallenge, [
'query' => [
'consent_challenge' => $consentChallenge,
],
] ]
] );
);
switch ($response->getStatusCode()) { $status = $response->getStatusCode();
case 404: if (503 === $status) {
throw new InvalidChallengeException(); ++$attempt;
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;
} }
@ -85,18 +101,18 @@ 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),
] ]
); );
return $response; return $response;
} }
@ -104,13 +120,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),
] ]
@ -123,13 +139,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 DatabaseConnectionException extends Exception class EmptyResultException extends Exception
{ {
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -103,6 +103,6 @@ class SQLLoginRequest
{ {
$fields = join(',', array_merge($this->getPasswordFields(), $this->getDataFields())); $fields = join(',', array_merge($this->getPasswordFields(), $this->getDataFields()));
return 'SELECT '.$fields.' FROM '.$this->getTableName().' WHERE '.$this->getLoginColumnName().' = :'.$this->getLoginColumnName().';'; return 'SELECT '.$fields.' FROM '.$this->getTableName().' WHERE LOWER('.$this->getLoginColumnName().') = LOWER(:'.$this->getLoginColumnName().');';
} }
} }

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(); 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);
} }
} }
$completedPlainPassword = ''; $completedPlainPassword = '';

View File

@ -5,12 +5,13 @@ 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 Psr\Log\LoggerInterface;
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,11 +26,7 @@ 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 ERROR_PDO = 'error_pdo'; public const TECHNICAL_ERROR = 'technical_error';
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;
@ -37,12 +34,14 @@ class SQLLoginUserAuthenticator extends AbstractLoginFormAuthenticator
string $baseUrl, string $baseUrl,
private SQLLoginService $sqlLoginService, private SQLLoginService $sqlLoginService,
private PasswordEncoder $passwordHasher, private PasswordEncoder $passwordHasher,
private SQLLoginRequest $sqlLoginRequest private SQLLoginRequest $sqlLoginRequest,
private LoggerInterface $logger
) { ) {
$this->baseUrl = $baseUrl; $this->baseUrl = $baseUrl;
$this->sqlLoginService = $sqlLoginService; $this->sqlLoginService = $sqlLoginService;
$this->passwordHasher = $passwordHasher; $this->passwordHasher = $passwordHasher;
$this->sqlLoginRequest = $sqlLoginRequest; $this->sqlLoginRequest = $sqlLoginRequest;
$this->logger = $logger;
} }
/** /**
@ -57,14 +56,14 @@ class SQLLoginUserAuthenticator extends AbstractLoginFormAuthenticator
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): RedirectResponse 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): RedirectResponse public function onAuthenticationFailure(Request $request, AuthenticationException $exception): RedirectResponse
{ {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse($this->baseUrl.'/login'); return new RedirectResponse($this->baseUrl . '/login');
} }
public function authenticate(Request $request): SelfValidatingPassport public function authenticate(Request $request): SelfValidatingPassport
@ -76,52 +75,54 @@ class SQLLoginUserAuthenticator extends AbstractLoginFormAuthenticator
$session = $request->getSession(); $session = $request->getSession();
try { try {
$datas = $this->sqlLoginService->fetchPasswordAndDatas($login); $datas = $this->sqlLoginService->fetchPasswordAndDatas($login);
$remoteHashedPassword = $datas[$this->sqlLoginRequest->getPasswordColumnName()]; } catch (EmptyResultException $e) {
unset($datas[$this->sqlLoginRequest->getPasswordColumnName()]); $session->set(self::ERROR_LOGIN, true);
$remoteSalt = null; $this->logger->warning("authentication failed", ['username' => $login, "remote_address" => $request->getClientIp()]);
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 (LoginElementsConfigurationException $e) { } catch (DataToFetchConfigurationException | PDOException $e) {
$session->set(self::ERROR_CONFIGURATION, true); \Sentry\captureException($e);
throw new AuthenticationException(); $session->set(self::TECHNICAL_ERROR, true);
} 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);
$this->logger->warning("authentication failed", ['username' => $login, "remote_address" => $request->getClientIp()]);
throw new AuthenticationException(); throw new AuthenticationException();
} catch (SecurityPatternConfigurationException $e) { } catch (SecurityPatternConfigurationException $e) {
$session->set(self::ERROR_SECURITY_PATTERN_CONFIGURATION, true); \Sentry\captureException($e);
$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());
$this->logger->warning("authentication succeeded", ['username' => $login, "remote_address" => $request->getClientIp()]);
return $passport;
} }
protected function getLoginUrl(Request $request): string protected function getLoginUrl(Request $request): string
{ {
return $this->baseUrl.'/login'; return $this->baseUrl . '/login';
} }
} }

View File

@ -2,20 +2,16 @@
namespace App\Service; namespace App\Service;
use App\SQLLogin\Exception\DatabaseConnectionException; use App\SQLLogin\Exception\EmptyResultException;
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
{ {
private PDO $pdo; public const MAX_RETRY = 3;
public function __construct( public function __construct(
private SQLLoginRequest $sqlLoginRequest, private SQLLoginRequest $sqlLoginRequest,
@ -23,31 +19,35 @@ 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
{ {
try { $dataRequest = $this->sqlLoginRequest->getDatasRequest();
$dataRequest = $this->sqlLoginRequest->getDatasRequest(); $datas = $this->executeRequestWithLogin($dataRequest, $login);
$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
{ {
$query = $this->pdo->prepare($request); $attempt = 0;
$query->bindParam($this->sqlLoginRequest->getLoginColumnName(), $login, PDO::PARAM_STR); while ($attempt < self::MAX_RETRY) {
$query->execute(); $pdo = $this->getConnection();
$datas = $query->fetch(PDO::FETCH_ASSOC); $query = $pdo->prepare($request);
$query->closeCursor(); $query->execute([$this->sqlLoginRequest->getLoginColumnName() => $login]);
$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,13 +55,8 @@ class SQLLoginService extends AbstractController
private function getConnection(): PDO private function getConnection(): PDO
{ {
// Appel du singleton // Appel du singleton
try { $sqlLogin = SQLLoginConnect::getInstance();
$sqlLogin = SQLLoginConnect::getInstance(); $pdo = $sqlLogin->connect($this->sqlLoginRequest->getDatabaseDsn(), $this->sqlLoginRequest->getDbUser(), $this->sqlLoginRequest->getDbPassword());
$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

@ -114,6 +114,18 @@
"config/packages/lock.yaml" "config/packages/lock.yaml"
] ]
}, },
"symfony/monolog-bundle": {
"version": "3.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.7",
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
},
"files": [
"config/packages/monolog.yaml"
]
},
"symfony/routing": { "symfony/routing": {
"version": "5.4", "version": "5.4",
"recipe": { "recipe": {

View File

@ -9,25 +9,9 @@
<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="36t19qm" resname="error.sql_login"> <trans-unit id="1QRR4uA" resname="error.technical">
<source>error.sql_login</source> <source>error.technical</source>
<target>Connection to database encountered a problem</target> <target>A technical error happened, try again later</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,25 +9,9 @@
<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="36t19qm" resname="error.sql_login"> <trans-unit id="1QRR4uA" resname="error.technical">
<source>error.sql_login</source> <source>error.technical</source>
<target>La connexion à la base de données a rencontré un problème</target> <target>Une erreur technique s'est produite, rééssayez plus tard</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>