maj: sémantique, révision vérification password #3
5
.env
|
@ -29,8 +29,9 @@ BASE_URL='http://localhost:8080'
|
||||||
# connexion hydra
|
# connexion hydra
|
||||||
HYDRA_ADMIN_BASE_URL='http://hydra:4445'
|
HYDRA_ADMIN_BASE_URL='http://hydra:4445'
|
||||||
APP_LOCALES="fr,en"
|
APP_LOCALES="fr,en"
|
||||||
|
SECURITY_PATTERN=
|
||||||
|
NEW_HASH_ALGO=
|
||||||
|
HASH_ALGO_LEGACY="sha256"
|
||||||
###> symfony/lock ###
|
###> symfony/lock ###
|
||||||
# Choose one of the stores below
|
# Choose one of the stores below
|
||||||
# postgresql+advisory://db_user:db_password@localhost/db_name
|
# postgresql+advisory://db_user:db_password@localhost/db_name
|
||||||
|
|
|
@ -6,9 +6,7 @@
|
||||||
/config/secrets/prod/prod.decrypt.private.php
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
/public/bundles/
|
/public/bundles/
|
||||||
/var/
|
/var/
|
||||||
###< symfony/framework-bundle ###
|
|
||||||
supervisord.log
|
|
||||||
supervisord.pid
|
|
||||||
/vendor
|
/vendor
|
||||||
/tools/php-cs-fixer/vendor
|
/tools/php-cs-fixer/vendor
|
||||||
###> symfony/webpack-encore-bundle ###
|
###> symfony/webpack-encore-bundle ###
|
||||||
|
@ -22,5 +20,5 @@ yarn-error.log
|
||||||
/.config
|
/.config
|
||||||
/.npm
|
/.npm
|
||||||
/.local
|
/.local
|
||||||
supervisord.log
|
/supervisord.log
|
||||||
supervisord.pid
|
/supervisord.pid
|
|
@ -10,7 +10,7 @@ security:
|
||||||
# algorithm: 'sha256'
|
# algorithm: 'sha256'
|
||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
pdo_user_provider:
|
sql_login_provider:
|
||||||
id: App\Security\SQLLoginUserProvider
|
id: App\Security\SQLLoginUserProvider
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
|
@ -19,7 +19,7 @@ security:
|
||||||
main:
|
main:
|
||||||
# lazy: true
|
# lazy: true
|
||||||
stateless: false
|
stateless: false
|
||||||
provider: pdo_user_provider
|
provider: sql_login_provider
|
||||||
custom_authenticators:
|
custom_authenticators:
|
||||||
- App\Security\SQLLoginUserAuthenticator
|
- App\Security\SQLLoginUserAuthenticator
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,12 @@ parameters:
|
||||||
database.user: "%env(resolve:DB_USER)%"
|
database.user: "%env(resolve:DB_USER)%"
|
||||||
database.password: "%env(resolve:DB_PASSWORD)%"
|
database.password: "%env(resolve:DB_PASSWORD)%"
|
||||||
|
|
||||||
# algorythme de hahshage utilisé "md5", "sha256", "haval160,4", etc.
|
# algorythme de hashage utilisé "md5", "sha256", "haval160,4", etc.
|
||||||
hashAlgo: "sha256"
|
env(HASH_ALGO_LEGACY): "sha256"
|
||||||
|
hashAlgoLegacy: '%env(resolve:HASH_ALGO_LEGACY)%'
|
||||||
|
|
||||||
|
env(NEW_HASH_ALGO): "sha256"
|
||||||
|
newHashAlgo: '%env(resolve:NEW_HASH_ALGO)%'
|
||||||
|
|
||||||
# adresse du site hote
|
# adresse du site hote
|
||||||
issuer_url: '%env(resolve:ISSUER_URL)%'
|
issuer_url: '%env(resolve:ISSUER_URL)%'
|
||||||
|
@ -21,6 +25,7 @@ parameters:
|
||||||
default_locale: '%env(DEFAULT_LOCALE)%'
|
default_locale: '%env(DEFAULT_LOCALE)%'
|
||||||
env(DEFAULT_LOCALE): 'fr'
|
env(DEFAULT_LOCALE): 'fr'
|
||||||
|
|
||||||
|
security_pattern: '%env(resolve:SECURITY_PATTERN)%'
|
||||||
|
|||||||
env(APP_LOCALES): "fr,en"
|
env(APP_LOCALES): "fr,en"
|
||||||
locales: '%env(APP_LOCALES)%'
|
locales: '%env(APP_LOCALES)%'
|
||||||
app.supported_locales: ~
|
app.supported_locales: ~
|
||||||
|
@ -65,6 +70,8 @@ services:
|
||||||
App\Security\Hasher\PasswordEncoder:
|
App\Security\Hasher\PasswordEncoder:
|
||||||
arguments:
|
arguments:
|
||||||
$pepper: '%pepper%'
|
$pepper: '%pepper%'
|
||||||
$hashAlgo: '%hashAlgo%'
|
$hashAlgoLegacy: '%hashAlgoLegacy%'
|
||||||
|
$newHashAlgo: '%newHashAlgo%'
|
||||||
|
$securityPattern: '%security_pattern%'
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|
|
@ -10,6 +10,6 @@ INSERT INTO usager (email, password, salt, lastname, firstname) VALUES
|
||||||
('test3@test.com', '504ae1c3e2f5fdaf41f868164dabcef21e17059f5f388b452718a1ce92692c67', 'cesaltestunautreexemple', 'Dupont', 'Henri');
|
('test3@test.com', '504ae1c3e2f5fdaf41f868164dabcef21e17059f5f388b452718a1ce92692c67', 'cesaltestunautreexemple', 'Dupont', 'Henri');
|
||||||
|
|
||||||
INSERT INTO usager (email, password, lastname, firstname) VALUES
|
INSERT INTO usager (email, password, lastname, firstname) VALUES
|
||||||
('test4@test.com', '50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d', 'Durand', 'Isabelle'),
|
('test4@test.com', '$2a$12$zFN0VJ..Cuu.2itWQwmHJe5EUhNHazbMfCSJFpNiEfdwpLzjjDM0u', 'Durand', 'Isabelle'),
|
||||||
('test2@test.com', '50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d', 'Dubois', 'Angela');
|
('test2@test.com', '50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d', 'Dubois', 'Angela');
|
||||||
GRANT ALL PRIVILEGES ON DATABASE usager TO lasql
|
GRANT ALL PRIVILEGES ON DATABASE usager TO lasql
|
||||||
|
|
|
@ -37,6 +37,9 @@ services:
|
||||||
- DB_PASSWORD=lasql
|
- DB_PASSWORD=lasql
|
||||||
- DEFAULT_LOCALE=fr
|
- DEFAULT_LOCALE=fr
|
||||||
- DSN_REMOTE_DATABASE=pgsql:host='postgres';port=5432;dbname=lasql;
|
- DSN_REMOTE_DATABASE=pgsql:host='postgres';port=5432;dbname=lasql;
|
||||||
|
- HASH_ALGO_LEGACY=sha256
|
||||||
|
- NEW_HASH_ALGO=bcrypt
|
||||||
|
- SECURITY_PATTERN=password,salt,pepper
|
||||||
|
|
||||||
|
|
||||||
oidc-test:
|
oidc-test:
|
||||||
|
|
|
@ -34,6 +34,10 @@ BASE_URL='http://localhost:8080'
|
||||||
HYDRA_ADMIN_BASE_URL='http://hydra:4445'
|
HYDRA_ADMIN_BASE_URL='http://hydra:4445'
|
||||||
DSN_REMOTE_DATABASE="pgsql:host='postgres';port=5432;dbname=lasql"
|
DSN_REMOTE_DATABASE="pgsql:host='postgres';port=5432;dbname=lasql"
|
||||||
APP_LOCALES="fr,en"
|
APP_LOCALES="fr,en"
|
||||||
|
NEW_HASH_ALGO="argon2id"
|
||||||
|
HASH_ALGO_LEGACY="sha256, bcrypt"
|
||||||
|
SECURITY_PATTERN="password,salt,pepper"
|
||||||
|
PEPPER
|
||||||
```
|
```
|
||||||
## Tests password
|
## Tests password
|
||||||
|
|
||||||
|
@ -69,7 +73,9 @@ DSN_REMOTE_DATABASE="mysql:host=mariadb;port=3306;dbname=lasql;"
|
||||||
|test1@test.com| 8ad4025044b77ae6a5e3fcf99e53e44b15db9a4ecf468be21cbc6b9fbdae6d9f| cesaltestunexemple| Locke|John|
|
|test1@test.com| 8ad4025044b77ae6a5e3fcf99e53e44b15db9a4ecf468be21cbc6b9fbdae6d9f| cesaltestunexemple| Locke|John|
|
||||||
|test2@test.com| 50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d| NULL| Dubois| Angela|
|
|test2@test.com| 50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d| NULL| Dubois| Angela|
|
||||||
|test3@test.com| 504ae1c3e2f5fdaf41f868164dabcef21e17059f5f388b452718a1ce92692c67| cesaltestunautreexemple| Dupont| Henri|
|
|test3@test.com| 504ae1c3e2f5fdaf41f868164dabcef21e17059f5f388b452718a1ce92692c67| cesaltestunautreexemple| Dupont| Henri|
|
||||||
|test4@test.com| 50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d| NULL| Durand|Isabelle|
|
|test4@test.com| $2a$12$zFN0VJ..Cuu.2itWQwmHJe5EUhNHazbMfCSJFpNiEfdwpLzjjDM0u| NULL| Durand|Isabelle|
|
||||||
|
|
||||||
|
A noter que le hash de test4 est en bcrypt
|
||||||
```
|
```
|
||||||
|
|
||||||
### mariadb (sans salt)
|
### mariadb (sans salt)
|
||||||
|
|
|
@ -36,9 +36,9 @@ class SecurityController extends AbstractController
|
||||||
$loginForm->get('password')->addError(new FormError($trans->trans('error.password', [], 'messages')));
|
$loginForm->get('password')->addError(new FormError($trans->trans('error.password', [], 'messages')));
|
||||||
$request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PASSWORD);
|
$request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PASSWORD);
|
||||||
}
|
}
|
||||||
if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_PDO)) {
|
if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_SQL_LOGIN)) {
|
||||||
$loginForm->addError(new FormError($trans->trans('error.pdo', [], 'messages')));
|
$loginForm->addError(new FormError($trans->trans('error.sql_login', [], 'messages')));
|
||||||
$request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PDO);
|
$request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_SQL_LOGIN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,18 +10,18 @@ use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||||
class SQLLoginExtension extends Extension implements CompilerPassInterface
|
class SQLLoginExtension extends Extension implements CompilerPassInterface
|
||||||
{
|
{
|
||||||
/** @var array */
|
/** @var array */
|
||||||
protected $pdoConfig;
|
protected $sqlLoginConfig;
|
||||||
|
|
||||||
public function load(array $configs, ContainerBuilder $container)
|
public function load(array $configs, ContainerBuilder $container)
|
||||||
{
|
{
|
||||||
$configuration = new SQLLoginConfiguration();
|
$configuration = new SQLLoginConfiguration();
|
||||||
$config = $this->processConfiguration($configuration, $configs);
|
$config = $this->processConfiguration($configuration, $configs);
|
||||||
$this->pdoConfig = $config;
|
$this->sqlLoginConfig = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(ContainerBuilder $container)
|
public function process(ContainerBuilder $container)
|
||||||
{
|
{
|
||||||
$definition = $container->getDefinition(SQLLoginRequest::class);
|
$definition = $container->getDefinition(SQLLoginRequest::class);
|
||||||
$definition->replaceArgument('$config', $this->pdoConfig);
|
$definition->replaceArgument('$config', $this->sqlLoginConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\SQLLogin\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class InvalidSQLLoginAlgoException extends Exception
|
||||||
|
{
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\SQLLogin\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class InvalidSQLLoginConfigurationException extends Exception
|
||||||
|
{
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ class SQLLoginConnect extends AbstractController
|
||||||
*
|
*
|
||||||
* @param void
|
* @param void
|
||||||
*
|
*
|
||||||
* @return PdoConnect
|
* @return SQLLoginConnect
|
||||||
*/
|
*/
|
||||||
public static function getInstance()
|
public static function getInstance()
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,6 +8,7 @@ class SQLLoginRequest
|
||||||
public const LOGIN_COLUMN_NAME = 'login_column_name';
|
public const LOGIN_COLUMN_NAME = 'login_column_name';
|
||||||
public const SALT_COLUMN_NAME = 'salt_column_name';
|
public const SALT_COLUMN_NAME = 'salt_column_name';
|
||||||
public const PASSWORD_COLUMN_NAME = 'password_column_name';
|
public const PASSWORD_COLUMN_NAME = 'password_column_name';
|
||||||
|
public const PASSWORD_NEED_UPGRADE = 'password_need_upgrade';
|
||||||
public const TABLE_NAME = 'table_name';
|
public const TABLE_NAME = 'table_name';
|
||||||
|
|
||||||
protected array $config;
|
protected array $config;
|
||||||
|
@ -23,36 +24,46 @@ class SQLLoginRequest
|
||||||
$this->password = $password;
|
$this->password = $password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDatabaseDsn()
|
public function getDatabaseDsn(): string
|
||||||
{
|
{
|
||||||
return $this->dsn;
|
return $this->dsn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDbUser()
|
public function getDbUser(): string
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDbPassword()
|
public function getDbPassword(): string
|
||||||
{
|
{
|
||||||
return $this->password;
|
return $this->password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLoginColumnName()
|
public function getLoginColumnName(): string
|
||||||
{
|
{
|
||||||
return $this->config[self::LOGIN_COLUMN_NAME];
|
return $this->config[self::LOGIN_COLUMN_NAME];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function egtPasswordColumnName()
|
public function getPasswordColumnName(): string
|
||||||
{
|
{
|
||||||
return $this->config[self::PASSWORD_COLUMN_NAME];
|
return $this->config[self::PASSWORD_COLUMN_NAME];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSaltColumnName()
|
public function getSaltColumnName(): ?string
|
||||||
{
|
{
|
||||||
return $this->config[self::SALT_COLUMN_NAME];
|
return $this->config[self::SALT_COLUMN_NAME];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTableName(): string
|
||||||
|
{
|
||||||
|
return $this->config[self::TABLE_NAME];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDataToFetch(): array
|
||||||
|
{
|
||||||
|
return $this->config[self::DATA_TO_FETCH];
|
||||||
|
}
|
||||||
|
|
||||||
public function getRequestScope()
|
public function getRequestScope()
|
||||||
{
|
{
|
||||||
$scope = '';
|
$scope = '';
|
||||||
|
@ -60,18 +71,32 @@ class SQLLoginRequest
|
||||||
$scope .= $data.',';
|
$scope .= $data.',';
|
||||||
}
|
}
|
||||||
$scope = substr($scope, 0, -1);
|
$scope = substr($scope, 0, -1);
|
||||||
$request = 'SELECT '.$scope.' FROM '.$this->config[self::TABLE_NAME].' WHERE '.$this->config[self::LOGIN_COLUMN_NAME].' = :'.$this->config[self::LOGIN_COLUMN_NAME].';';
|
|
||||||
|
|
||||||
return $request;
|
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()
|
||||||
{
|
{
|
||||||
$passwordColumns = $this->config[self::PASSWORD_COLUMN_NAME];
|
$fields = $this->getPasswordColumnName();
|
||||||
if (!empty($this->config[self::SALT_COLUMN_NAME])) {
|
if (!empty($this->getSaltColumnName())) {
|
||||||
$passwordColumns .= ', '.$this->config[self::SALT_COLUMN_NAME];
|
$fields .= ', '.$this->getSaltColumnName();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'SELECT '.$passwordColumns.' FROM '.$this->config[self::TABLE_NAME].' WHERE '.$this->config[self::LOGIN_COLUMN_NAME].' = :'.$this->config[self::LOGIN_COLUMN_NAME].';';
|
return 'SELECT '.$fields.' FROM '.$this->getTableName().' WHERE '.$this->getLoginColumnName().' = :'.$this->getLoginColumnName().';';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequestUpdatePassword()
|
||||||
|
{
|
||||||
|
$fieldsToUpdate = $this->getPasswordColumnName().'= :'.$this->getPasswordColumnName().',';
|
||||||
|
if (!empty($this->getSaltColumnName())) {
|
||||||
|
$fieldsToUpdate .= $this->getSaltColumnName().'= :'.$this->getSaltColumnName().',';
|
||||||
|
}
|
||||||
|
$fieldsToUpdate = substr($fieldsToUpdate, 0, -1);
|
||||||
|
|
||||||
|
return 'UPDATE '.$this->getTableName().' SET '.$fieldsToUpdate.' WHERE '.$this->getLoginColumnName().' = :'.$this->getLoginColumnName().';';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Security\Hasher;
|
namespace App\Security\Hasher;
|
||||||
|
|
||||||
|
use App\SQLLogin\Exception\InvalidSQLLoginAlgoException;
|
||||||
|
use App\SQLLogin\Exception\InvalidSQLLoginConfigurationException;
|
||||||
use App\SQLLogin\Exception\InvalidSQLPasswordException;
|
use App\SQLLogin\Exception\InvalidSQLPasswordException;
|
||||||
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
|
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait;
|
use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait;
|
||||||
|
@ -10,23 +12,37 @@ use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
|
||||||
class PasswordEncoder implements LegacyPasswordHasherInterface
|
class PasswordEncoder implements LegacyPasswordHasherInterface
|
||||||
{
|
{
|
||||||
use CheckPasswordLengthTrait;
|
use CheckPasswordLengthTrait;
|
||||||
protected ?string $pepper;
|
public const PASSWORD_PATTERN = 'password';
|
||||||
protected string $hashAlgo;
|
public const SALT_PATTERN = 'salt';
|
||||||
|
public const PEPPER_PATTERN = 'pepper';
|
||||||
|
|
||||||
public function __construct(?string $pepper, string $hashAlgo)
|
protected ?string $pepper;
|
||||||
|
protected array $hashAlgoLegacy;
|
||||||
|
protected ?string $newHashAlgo;
|
||||||
|
protected array $securityPattern;
|
||||||
|
|
||||||
|
public function __construct(?string $pepper, string $hashAlgoLegacy, ?string $newHashAlgo, string $securityPattern)
|
||||||
{
|
{
|
||||||
$this->pepper = $pepper;
|
$this->pepper = $pepper;
|
||||||
$this->hashAlgo = $hashAlgo;
|
$this->hashAlgoLegacy = explode(',', $hashAlgoLegacy);
|
||||||
rmasson marked this conversation as resolved
Outdated
wpetit
commented
Je pense que ce code ne sera pas compatible avec les applications ayant choisies d'utiliser les algorithmes Normalement, en PHP on utilise les méthodes Autre chose: la méthode assume un certain format pour la concaténation des différents éléments à hacher ( Je pense que ce code ne sera pas compatible avec les applications ayant choisies d'utiliser les algorithmes `argon2id`, `scrypt` et `bcrypt` (qui sont d'ailleurs les recommandations OWASP [^1] aujourd'hui).
Normalement, en PHP on utilise les méthodes `password_hash()` [^2] et `password_verify()` [^3] pour utiliser ces algorithmes (et d'ailleurs le salt est directement stocké dans le hash final [^4]).
Autre chose: la méthode assume un certain format pour la concaténation des différents éléments à hacher (`$plainPassword.$salt.$this->pepper`). Il est fort peu probable que cette séquence soit toujours respectée dans toutes les applications. À mon avis il serait certainement préférable d'utiliser un patron (avec la méthode `strstr()` [^5] par exemple) pour permettre à l'utilisateur de spécifiquer le format de concaténation.
[^1]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
[^2]: https://www.php.net/manual/en/function.password-hash.php
[^3]: https://www.php.net/manual/en/function.password-verify.php
[^4]: https://stackoverflow.com/questions/40993645/understanding-bcrypt-salt-as-used-by-php-password-hash
[^5]: https://www.php.net/manual/en/function.strtr.php
rmasson
commented
j'ai utilisé les fonction password_algos() et hash_algos() pour faire la différence entre les algos, pour utiliser hash() ou password_hash() Mais effectivement le pattern est pas pris en compte j'ai utilisé les fonction password_algos() et hash_algos() pour faire la différence entre les algos, pour utiliser hash() ou password_hash()
Mais effectivement le pattern est pas pris en compte
|
|||||||
|
$this->newHashAlgo = $newHashAlgo;
|
||||||
|
$this->securityPattern = explode(',', $securityPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilise l'algo legacy
|
||||||
|
*/
|
||||||
public function hash(string $plainPassword, string $salt = null): string
|
public function hash(string $plainPassword, string $salt = null): string
|
||||||
{
|
{
|
||||||
if ($this->isPasswordTooLong($plainPassword)) {
|
if ($this->isPasswordTooLong($plainPassword)) {
|
||||||
throw new InvalidPasswordException();
|
throw new InvalidPasswordException();
|
||||||
wpetit
commented
Il ne faut pas utiliser l'opérateur Il serait mieux d'utiliser https://www.php.net/manual/en/function.hash-equals.php je pense dans ce cas. Il ne faut pas utiliser l'opérateur `===` pour faire des comparaisons de mots de passe (ça ouvre à des failles du type "Timing attack" [^1]).
Il serait mieux d'utiliser https://www.php.net/manual/en/function.hash-equals.php je pense dans ce cas.
[^1]: https://www.chosenplaintext.ca/articles/beginners-guide-constant-time-cryptography.html
|
|||||||
}
|
}
|
||||||
$hash = hash($this->hashAlgo, $plainPassword.$salt.$this->pepper);
|
$completedPlainPassword = $this->getPasswordToHash($plainPassword, $salt);
|
||||||
|
if ($this->isObsoleteAlgo($this->newHashAlgo)) {
|
||||||
|
throw new InvalidSQLLoginAlgoException();
|
||||||
|
}
|
||||||
|
|
||||||
return $hash;
|
return password_hash($completedPlainPassword, $this->getValidALgo($this->newHashAlgo));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verify(string $hashedPassword, string $plainPassword, string $salt = null): bool
|
public function verify(string $hashedPassword, string $plainPassword, string $salt = null): bool
|
||||||
|
@ -35,7 +51,36 @@ class PasswordEncoder implements LegacyPasswordHasherInterface
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hash($plainPassword, $salt) === $hashedPassword) {
|
$isNewest = password_get_info($hashedPassword)['algo'] === $this->newHashAlgo;
|
||||||
|
|
||||||
|
$completedPassword = $this->getPasswordToHash($plainPassword, $salt);
|
||||||
|
if ($isNewest) {
|
||||||
|
if (password_verify($completedPassword, $hashedPassword)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new InvalidSQLPasswordException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le mot de passe a besoin d'un rehash ou qu'il n'y a pas de nouvelle méthode indiquée, on sait qu'il faut utiliser l'une des méthodes legacy pour vérifier le mot de passe
|
||||||
|
if ($this->needsRehash($hashedPassword) || empty($this->newHashAlgo)) {
|
||||||
|
foreach ($this->hashAlgoLegacy as $algo) {
|
||||||
|
if ($this->isObsoleteAlgo($algo)) {
|
||||||
|
if (hash_equals(hash($algo, $completedPassword), $hashedPassword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (password_verify($completedPassword, $hashedPassword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// On vérifie si la méthode legacy est obsolète, si oui on ne peut pas utiliser password_verify, on doit hasher et comparer
|
||||||
|
}
|
||||||
|
// si on on n'a pas encore retourné de résultat, le mot de passe doit être incorrect
|
||||||
|
throw new InvalidSQLPasswordException();
|
||||||
|
}
|
||||||
|
// Si on n'est pas rentré dans les conditions précedéntes, on peut utiliser password_verify
|
||||||
|
if (password_verify($completedPassword, $hashedPassword)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidSQLPasswordException();
|
throw new InvalidSQLPasswordException();
|
||||||
|
@ -44,6 +89,74 @@ class PasswordEncoder implements LegacyPasswordHasherInterface
|
||||||
|
|
||||||
public function needsRehash(string $hashedPassword): bool
|
public function needsRehash(string $hashedPassword): bool
|
||||||
{
|
{
|
||||||
|
// Il y a besoin de tester si on veut mettre à jour le hashage de mot de passe uniquement si le nouveau algo de hashage est indiqué et qu'il est moderne (BCRYPT, ARGON, ...)
|
||||||
|
if (!empty($this->newHashAlgo) && !$this->isObsoleteAlgo($this->newHashAlgo)) {
|
||||||
|
return password_needs_rehash($hashedPassword, $this->getValidAlgo($this->newHashAlgo));
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $algo
|
||||||
|
*
|
||||||
|
* @return InvalidSQLLoginAlgoException|string
|
||||||
|
*/
|
||||||
|
public function getValidAlgo($algo)
|
||||||
|
{
|
||||||
|
if ($algo) {
|
||||||
|
if (in_array($algo, hash_algos())) {
|
||||||
|
return $algo;
|
||||||
|
}
|
||||||
|
$informalAlgos = [
|
||||||
|
'default' => PASSWORD_DEFAULT,
|
||||||
|
'bcrypt' => PASSWORD_BCRYPT,
|
||||||
|
'argon2i' => PASSWORD_ARGON2I,
|
||||||
|
'argon2id' => PASSWORD_ARGON2ID,
|
||||||
|
];
|
||||||
|
if (in_array($algo, array_keys($informalAlgos))) {
|
||||||
|
return $informalAlgos[$algo];
|
||||||
|
}
|
||||||
|
if (in_array($algo, password_algos())) {
|
||||||
|
return $algo;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidSQLLoginAlgoException('Invalid Algorythme');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isObsoleteAlgo($algo): bool
|
||||||
|
{
|
||||||
|
return in_array($algo, hash_algos());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la string à hasher en fonction du pattern indiqué
|
||||||
|
*
|
||||||
|
* @param mixed $plainTextPassword
|
||||||
|
* @param mixed $salt
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getPasswordToHash($plainTextPassword, $salt)
|
||||||
|
{
|
||||||
|
$arrayRef = [
|
||||||
|
self::PASSWORD_PATTERN => $plainTextPassword,
|
||||||
|
self::SALT_PATTERN => $salt,
|
||||||
|
self::PEPPER_PATTERN => $this->pepper,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->securityPattern as $term) {
|
||||||
|
if (self::PEPPER_PATTERN !== $term && self::PASSWORD_PATTERN !== $term && self::SALT_PATTERN !== $term) {
|
||||||
|
throw new InvalidSQLLoginConfigurationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$completedArray = [];
|
||||||
|
foreach ($this->securityPattern as $term) {
|
||||||
|
$completedArray[] = $arrayRef[$term];
|
||||||
|
}
|
||||||
|
$completedPlainPassword = implode($completedArray);
|
||||||
|
|
||||||
|
return $completedPlainPassword;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,17 +25,17 @@ class SQLLoginUserAuthenticator extends AbstractAuthenticator
|
||||||
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_PASSWORD = 'error_password';
|
public const ERROR_PASSWORD = 'error_password';
|
||||||
public const ERROR_PDO = 'error_pdo';
|
public const ERROR_SQL_LOGIN = 'error_sql_login';
|
||||||
|
|
||||||
protected string $baseUrl;
|
protected string $baseUrl;
|
||||||
private SQLLoginService $pdoService;
|
private SQLLoginService $sqlLoginService;
|
||||||
wpetit
commented
Je pense que la variable n'a pas été renommée Je pense que la variable n'a pas été renommée `$pdoService` en accord avec le changement de nom du service.
|
|||||||
private UrlGeneratorInterface $router;
|
private UrlGeneratorInterface $router;
|
||||||
private PasswordEncoder $passwordHasher;
|
private PasswordEncoder $passwordHasher;
|
||||||
|
|
||||||
public function __construct(string $baseUrl, SQLLoginService $pdoService, UrlGeneratorInterface $router, PasswordEncoder $passwordHasher)
|
public function __construct(string $baseUrl, SQLLoginService $sqlLoginService, UrlGeneratorInterface $router, PasswordEncoder $passwordHasher)
|
||||||
wpetit
commented
Idem, cf. commentaire précédent Idem, cf. [commentaire précédent](https://forge.cadoles.com/Cadoles/hydra-sql/pulls/3/files#issuecomment-55979)
|
|||||||
{
|
{
|
||||||
$this->baseUrl = $baseUrl;
|
$this->baseUrl = $baseUrl;
|
||||||
$this->pdoService = $pdoService;
|
$this->sqlLoginService = $sqlLoginService;
|
||||||
wpetit
commented
Idem, cf. commentaire précédent Idem, cf. [commentaire précédent](https://forge.cadoles.com/Cadoles/hydra-sql/pulls/3/files#issuecomment-55979)
|
|||||||
$this->router = $router;
|
$this->router = $router;
|
||||||
$this->passwordHasher = $passwordHasher;
|
$this->passwordHasher = $passwordHasher;
|
||||||
}
|
}
|
||||||
|
@ -72,16 +72,21 @@ class SQLLoginUserAuthenticator extends AbstractAuthenticator
|
||||||
$rememberMe = isset($form['_remember_me']) ? true : false;
|
$rememberMe = isset($form['_remember_me']) ? true : false;
|
||||||
try {
|
try {
|
||||||
// requête préparée
|
// requête préparée
|
||||||
list($remoteHashedPassword, $remoteSalt) = $this->pdoService->fetchPassword($login);
|
list($remoteHashedPassword, $remoteSalt) = $this->sqlLoginService->fetchPassword($login);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
$request->getSession()->set(self::ERROR_PDO, true);
|
$request->getSession()->set(self::ERROR_SQL_LOGIN, true);
|
||||||
throw new AuthenticationException();
|
throw new AuthenticationException();
|
||||||
}
|
}
|
||||||
if ($remoteHashedPassword) {
|
if ($remoteHashedPassword) {
|
||||||
try {
|
try {
|
||||||
// Comparaison remote hash et hash du input password + salt
|
// Comparaison remote hash et hash du input password + salt
|
||||||
|
// dump($remoteHashedPassword, $plaintextPassword, $remoteSalt, password_verify($plaintextPassword, $remoteHashedPassword));
|
||||||
$this->passwordHasher->verify($remoteHashedPassword, $plaintextPassword, $remoteSalt);
|
$this->passwordHasher->verify($remoteHashedPassword, $plaintextPassword, $remoteSalt);
|
||||||
$attributes = $this->pdoService->fetchDatas($login);
|
if ($this->passwordHasher->needsRehash($remoteHashedPassword)) {
|
||||||
|
$hash = $this->passwordHasher->hash($plaintextPassword);
|
||||||
|
$this->sqlLoginService->updatePassword($login, $hash, null);
|
||||||
|
}
|
||||||
|
$attributes = $this->sqlLoginService->fetchDatas($login);
|
||||||
$user = new User($login, $remoteHashedPassword, $attributes, $rememberMe);
|
$user = new User($login, $remoteHashedPassword, $attributes, $rememberMe);
|
||||||
|
|
||||||
$loader = function (string $userIdentifier) use ($user) {
|
$loader = function (string $userIdentifier) use ($user) {
|
||||||
|
@ -98,7 +103,7 @@ class SQLLoginUserAuthenticator extends AbstractAuthenticator
|
||||||
$request->getSession()->set(self::ERROR_PASSWORD, true);
|
$request->getSession()->set(self::ERROR_PASSWORD, true);
|
||||||
throw new AuthenticationException();
|
throw new AuthenticationException();
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
$request->getSession()->set(self::ERROR_PDO, true);
|
$request->getSession()->set(self::ERROR_SQL_LOGIN, true);
|
||||||
throw new AuthenticationException();
|
throw new AuthenticationException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ namespace App\Security;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
|
||||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
|
@ -13,10 +12,9 @@ class SQLLoginUserProvider implements UserProviderInterface
|
||||||
{
|
{
|
||||||
protected RequestStack $requestStack;
|
protected RequestStack $requestStack;
|
||||||
|
|
||||||
public function __construct(RequestStack $requestStack, SessionInterface $session)
|
public function __construct(RequestStack $requestStack)
|
||||||
{
|
{
|
||||||
$this->requestStack = $requestStack;
|
$this->requestStack = $requestStack;
|
||||||
$this->session = $session;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadUserByIdentifier(string $identifier, ?User $user): ?UserInterface
|
public function loadUserByIdentifier(string $identifier, ?User $user): ?UserInterface
|
||||||
|
|
|
@ -56,12 +56,11 @@ class SQLLoginService extends AbstractController
|
||||||
$password = $query->fetch(PDO::FETCH_ASSOC);
|
$password = $query->fetch(PDO::FETCH_ASSOC);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
\Sentry\captureException($e);
|
\Sentry\captureException($e);
|
||||||
dd($e);
|
|
||||||
throw new PDOException();
|
throw new PDOException();
|
||||||
}
|
}
|
||||||
if ($password) {
|
if ($password) {
|
||||||
return [
|
return [
|
||||||
$password[$this->sqlLoginRequest->egtPasswordColumnName()],
|
$password[$this->sqlLoginRequest->getPasswordColumnName()],
|
||||||
isset($password[$this->sqlLoginRequest->getSaltColumnName()]) ? $password[$this->sqlLoginRequest->getSaltColumnName()] : null,
|
isset($password[$this->sqlLoginRequest->getSaltColumnName()]) ? $password[$this->sqlLoginRequest->getSaltColumnName()] : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -69,11 +68,28 @@ class SQLLoginService extends AbstractController
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatePassword($login, $hashedPassword, $salt)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dbh = $this->getConnection();
|
||||||
|
$request = $this->sqlLoginRequest->getRequestUpdatePassword();
|
||||||
wpetit
commented
Idem, cf. commentaire précédent Idem, cf. [commentaire précédent](https://forge.cadoles.com/Cadoles/hydra-sql/pulls/3/files#issuecomment-55979)
|
|||||||
|
$query = $dbh->prepare($request);
|
||||||
|
$query->execute([
|
||||||
|
$this->sqlLoginRequest->getLoginColumnName() => $login,
|
||||||
|
$this->sqlLoginRequest->getPasswordColumnName() => $hashedPassword,
|
||||||
|
$this->sqlLoginRequest->getSaltColumnName() => $salt,
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
\Sentry\captureException($e);
|
||||||
|
throw new PDOException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function getConnection()
|
public function getConnection()
|
||||||
{
|
{
|
||||||
// Appel du singleton
|
// Appel du singleton
|
||||||
$pdo = SQLLoginConnect::getInstance();
|
$sqlLogin = SQLLoginConnect::getInstance();
|
||||||
// Connection bdd
|
// Connection bdd
|
||||||
return $pdo->connect($this->sqlLoginRequest->getDatabaseDsn(), $this->sqlLoginRequest->getDbUser(), $this->sqlLoginRequest->getDbPassword());
|
return $sqlLogin->connect($this->sqlLoginRequest->getDatabaseDsn(), $this->sqlLoginRequest->getDbUser(), $this->sqlLoginRequest->getDbPassword());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
error:
|
error:
|
||||||
login: 'Incorrect login'
|
login: 'Incorrect login'
|
||||||
password: 'Incorrect password'
|
password: 'Incorrect password'
|
||||||
pdo: 'Connection to database encountered a problem'
|
sql_login: 'Connection to database encountered a problem'
|
|
@ -1,4 +1,4 @@
|
||||||
error:
|
error:
|
||||||
login: 'Login incorrect ou inconnu'
|
login: 'Login incorrect ou inconnu'
|
||||||
password: 'Mot de passe incorrect'
|
password: 'Mot de passe incorrect'
|
||||||
pdo: 'La connexion à la base de données a rencontré un problème'
|
sql_login: 'La connexion à la base de données a rencontré un problème'
|
Hmm, je pense que par défaut le pepper devrait être vide.