maj: sémantique, révision vérification password #3

Merged
cmsassot merged 6 commits from maj into develop 2023-01-05 14:09:41 +01:00
28 changed files with 314 additions and 207 deletions
Showing only changes of commit 441c0f563c - Show all commits

View File

@ -2,11 +2,16 @@ security:
enable_authenticator_manager: true enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers: password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' app_hasher:
# the service ID of your custom hasher (the FQCN using the default services.yaml)
id: 'App\Security\Hasher\PasswordEncoder'
# App\Entity\User: 'sha256'
# Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
# 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: pdo_user_provider:
id: App\Security\PdoUserProvider id: App\Security\SQLLoginUserProvider
firewalls: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
@ -16,7 +21,7 @@ security:
stateless: false stateless: false
provider: pdo_user_provider provider: pdo_user_provider
custom_authenticators: custom_authenticators:
- App\Security\PdoUserAuthenticator - App\Security\SQLLoginUserAuthenticator
entry_point: form_login entry_point: form_login
form_login: form_login:

View File

@ -1,9 +1,9 @@
twig: twig:
globals:
locales: "%app.supported_locales%"
default_path: '%kernel.project_dir%/templates' default_path: '%kernel.project_dir%/templates'
form_themes: form_themes:
- 'bootstrap_5_layout.html.twig' - 'bootstrap_5_layout.html.twig'
globals:
locales: "%app.supported_locales%"
when@test: when@test:
twig: twig:
strict_variables: true strict_variables: true

View File

@ -1,8 +0,0 @@
pdo:
column_login_name: email
column_password_name: password
table_name: usager
data_to_fetch:
- email
- lastname
- firstname

View File

@ -24,6 +24,9 @@ parameters:
env(APP_LOCALES): "fr,en" env(APP_LOCALES): "fr,en"
locales: '%env(APP_LOCALES)%' locales: '%env(APP_LOCALES)%'
app.supported_locales: ~ app.supported_locales: ~
env(PEPPER): "257d62c24cd352c21b51c26dba678c8ff05011a89022aec106185bf67c69aa8b"

Hmm, je pense que par défaut le pepper devrait être vide.

Hmm, je pense que par défaut le pepper devrait être vide.
pepper: '%env(resolve:PEPPER)%'
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
_defaults: _defaults:
@ -39,11 +42,11 @@ services:
- '../src/Entity/' - '../src/Entity/'
- '../src/Kernel.php' - '../src/Kernel.php'
App\Security\PdoUserAuthenticator: App\Security\SQLLoginUserAuthenticator:
arguments: arguments:
$baseUrl: '%base_url%' $baseUrl: '%base_url%'
App\Pdo\PdoRequest: App\SQLLogin\SQLLoginRequest:
arguments: arguments:
$config: [] $config: []
$dsn: "%database.dsn%" $dsn: "%database.dsn%"
@ -58,5 +61,10 @@ services:
App\EventListener\LocaleSubscriber: App\EventListener\LocaleSubscriber:
arguments: arguments:
$defaultLocale: "%default_locale%" $defaultLocale: "%default_locale%"
App\Security\Hasher\PasswordEncoder:
arguments:
$pepper: '%pepper%'
$hashAlgo: '%hashAlgo%'
# 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

View File

@ -0,0 +1,9 @@
sql_login:
login_column_name: email
password_column_name: password
salt_column_name: salt
table_name: usager
data_to_fetch:
- email
- lastname
- firstname

View File

@ -1,12 +1,15 @@
CREATE TABLE IF NOT EXISTS usager ( CREATE TABLE IF NOT EXISTS usager (
email VARCHAR ( 100 ) UNIQUE NOT NULL, email VARCHAR ( 100 ) UNIQUE NOT NULL,
password VARCHAR ( 255 ) NOT NULL, password VARCHAR ( 255 ) NOT NULL,
salt VARCHAR (255),
lastname VARCHAR ( 255 ) NOT NULL, lastname VARCHAR ( 255 ) NOT NULL,
firstname VARCHAR ( 255 ) NOT NULL firstname VARCHAR ( 255 ) NOT NULL
); );
INSERT INTO usager (email, password, salt, lastname, firstname) VALUES
('test1@test.com', '8ad4025044b77ae6a5e3fcf99e53e44b15db9a4ecf468be21cbc6b9fbdae6d9f', 'cesaltestunexemple', 'Locke', 'John'),
('test3@test.com', '504ae1c3e2f5fdaf41f868164dabcef21e17059f5f388b452718a1ce92692c67', 'cesaltestunautreexemple', 'Dupont', 'Henri');
INSERT INTO usager (email, password, lastname, firstname) VALUES INSERT INTO usager (email, password, lastname, firstname) VALUES
('test1@test.com', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'Locke', 'John'), ('test4@test.com', '50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d', 'Durand', 'Isabelle'),
('test2@test.com', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'Dubois', 'Angela'), ('test2@test.com', '50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d', 'Dubois', 'Angela');
('test3@test.com', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'Dupont', 'Henri'),
('test4@test.com', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'Durand', 'Isabelle');
GRANT ALL PRIVILEGES ON DATABASE usager TO lasql GRANT ALL PRIVILEGES ON DATABASE usager TO lasql

View File

@ -35,7 +35,25 @@ 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"
``` ```
## Tests password
### Postgres
```
Les mot de passe inscrits en bdd sont hachés en tenant compte du salt si non vide (cf données de la bdd plus bas) et du pepper inscrit en variable d'environnement (généré avec : bin2hex(random_bytes(32))
Il faut inscrire dans slq_login_configuration salt_column_name: salt
et conserver le pepper dans service.yaml
env(PEPPER): "257d62c24cd352c21b51c26dba678c8ff05011a89022aec106185bf67c69aa8b"
```
### mariadb
```
En plus de tester la connexion à une différente base de donnée, on teste le hashage de password sans salt ni pepper
Il faut mettre dans sql_login_configuration salt_column_name: ~
et dans service.yaml
env(PEPPER): ~
```
### test connexion mariadb ### test connexion mariadb
``` ```
Modifier la variable d'environnement avec cette valeur Modifier la variable d'environnement avec cette valeur
@ -45,13 +63,23 @@ DSN_REMOTE_DATABASE="mysql:host=mariadb;port=3306;dbname=lasql;"
## Données de test ## Données de test
un base de données postgres est montée dans l'environnement pour tester la connexion: ### postgres
```
|email|password|salt|lastname|firstname
|test1@test.com| 8ad4025044b77ae6a5e3fcf99e53e44b15db9a4ecf468be21cbc6b9fbdae6d9f| cesaltestunexemple| Locke|John|
|test2@test.com| 50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d| NULL| Dubois| Angela|
|test3@test.com| 504ae1c3e2f5fdaf41f868164dabcef21e17059f5f388b452718a1ce92692c67| cesaltestunautreexemple| Dupont| Henri|
|test4@test.com| 50626fa21f45a275cea0efff13ff78fd02234cade322da08b7191c7e9150141d| NULL| Durand|Isabelle|
```
utilisateurs disponibles au démarage: ### mariadb (sans salt)
('test1@test.com', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'Locke', 'John'), ```
('test2@test.com', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'Dubois', 'Angela'), |email|password|lastname|firstname
('test3@test.com', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'Dupont', 'Henri'), |test1@test.com| 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92| Sassot|Charles|
('test4@test.com', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'Durand', 'Isabelle'); |test2@test.com| 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92| Dubois| Angela|
|test3@test.com| 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92| Dupont| Henri|
|test4@test.com| 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92| Durand|Isabelle|
```
Mot de passe = '123456' hashé en par sha256 Mot de passe = '123456' hashé en par sha256
@ -61,8 +89,9 @@ Permet d'adapter les requetes SQL en indiquant les noms de colonnes nécessaires
pdo_configuration/pdo.yaml pdo_configuration/pdo.yaml
``` ```
pdo: pdo:
column_login_name: email login_column_name: email
column_password_name: password password_column_name: password
salt_column_name: ~
table_name: usager table_name: usager
data_to_fetch: data_to_fetch:
- email - email

View File

@ -3,14 +3,25 @@
namespace App\Controller; namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
class LocaleController extends AbstractController class LocaleController extends AbstractController
{ {
#[Route(path: 'locale/{locale}', name: 'locale_change')] private ParameterBagInterface $params;
public function changeLocal(string $locale, Request $request)
public function __construct(ParameterBagInterface $params)
{ {
$this->params = $params;
}
#[Route(path: 'locale/{locale?}', name: 'locale_change')]
public function changeLocal(?string $locale, Request $request)
{
if (empty($locale)) {
$locale = $this->params->get('default_locale');
}
// On stocke la langue dans la session // On stocke la langue dans la session
$request->getSession()->set('_locale', $locale); $request->getSession()->set('_locale', $locale);

View File

@ -3,7 +3,7 @@
namespace App\Controller; namespace App\Controller;
use App\Form\LoginType; use App\Form\LoginType;
use App\Security\PdoUserAuthenticator; use App\Security\SQLLoginUserAuthenticator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
@ -28,17 +28,17 @@ class SecurityController extends AbstractController
$loginForm = $this->createForm(LoginType::class, null); $loginForm = $this->createForm(LoginType::class, null);
$error = $authenticationUtils->getLastAuthenticationError(); $error = $authenticationUtils->getLastAuthenticationError();
if ($error) { if ($error) {
if ($request->getSession()->has(PdoUserAuthenticator::ERROR_LOGIN)) { if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_LOGIN)) {
$loginForm->get('login')->addError(new FormError($trans->trans('error.login', [], 'messages'))); $loginForm->get('login')->addError(new FormError($trans->trans('error.login', [], 'messages')));
$request->getSession()->remove(PdoUserAuthenticator::ERROR_LOGIN); $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_LOGIN);
} }
if ($request->getSession()->has(PdoUserAuthenticator::ERROR_PASSWORD)) { if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_PASSWORD)) {
$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(PdoUserAuthenticator::ERROR_PASSWORD); $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PASSWORD);
} }
if ($request->getSession()->has(PdoUserAuthenticator::ERROR_PDO)) { if ($request->getSession()->has(SQLLoginUserAuthenticator::ERROR_PDO)) {
$loginForm->addError(new FormError($trans->trans('error.pdo', [], 'messages'))); $loginForm->addError(new FormError($trans->trans('error.pdo', [], 'messages')));
$request->getSession()->remove(PdoUserAuthenticator::ERROR_PDO); $request->getSession()->remove(SQLLoginUserAuthenticator::ERROR_PDO);
} }
} }

View File

@ -1,25 +0,0 @@
<?php
namespace App\DependencyInjection;
use App\Pdo\PdoRequest;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class PdoConfiguration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('pdo');
$treeBuilder->getRootNode()->children()
->scalarNode(PdoRequest::COLUMN_LOGIN_NAME)->isRequired()->cannotBeEmpty()->end()
->scalarNode(PdoRequest::COLUMN_PASSWORD_NAME)->isRequired()->cannotBeEmpty()->end()
->scalarNode(PdoRequest::TABLE_NAME)->isRequired()->cannotBeEmpty()->end()
->arrayNode(PdoRequest::DATA_TO_FETCH)
->scalarPrototype()->end()
->end()
->end();
return $treeBuilder;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\DependencyInjection;
use App\SQLLogin\SQLLoginRequest;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class SQLLoginConfiguration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('sql_login');
$treeBuilder->getRootNode()->children()
->scalarNode(SQLLoginRequest::LOGIN_COLUMN_NAME)->isRequired()->cannotBeEmpty()->end()
->scalarNode(SQLLoginRequest::PASSWORD_COLUMN_NAME)->isRequired()->cannotBeEmpty()->end()
->scalarNode(SQLLoginRequest::SALT_COLUMN_NAME)->end()
->scalarNode(SQLLoginRequest::TABLE_NAME)->isRequired()->cannotBeEmpty()->end()
->arrayNode(SQLLoginRequest::DATA_TO_FETCH)
->scalarPrototype()->end()
->end()
->end();
return $treeBuilder;
}
}

View File

@ -2,26 +2,26 @@
namespace App\DependencyInjection; namespace App\DependencyInjection;
use App\Pdo\PdoRequest; use App\SQLLogin\SQLLoginRequest;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\Extension;
class PdoExtension extends Extension implements CompilerPassInterface class SQLLoginExtension extends Extension implements CompilerPassInterface
{ {
/** @var array */ /** @var array */
protected $pdoConfig; protected $pdoConfig;
public function load(array $configs, ContainerBuilder $container) public function load(array $configs, ContainerBuilder $container)
{ {
$configuration = new PdoConfiguration(); $configuration = new SQLLoginConfiguration();
$config = $this->processConfiguration($configuration, $configs); $config = $this->processConfiguration($configuration, $configs);
$this->pdoConfig = $config; $this->pdoConfig = $config;
} }
public function process(ContainerBuilder $container) public function process(ContainerBuilder $container)
{ {
$definition = $container->getDefinition(PdoRequest::class); $definition = $container->getDefinition(SQLLoginRequest::class);
$definition->replaceArgument('$config', $this->pdoConfig); $definition->replaceArgument('$config', $this->pdoConfig);
} }
} }

View File

@ -6,8 +6,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface class User implements UserInterface
{ {
/** @var array */ protected array $attributes = [];
protected $attributes;
private string $login; private string $login;
private string $password; private string $password;
private bool $rememberMe; private bool $rememberMe;

View File

@ -4,15 +4,16 @@ namespace App\EventListener;
use App\Hydra\HydraService; use App\Hydra\HydraService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent; use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutSubscriber implements EventSubscriberInterface class LogoutSubscriber implements EventSubscriberInterface
{ {
private HydraService $hydra;
public function __construct( public function __construct(
private UrlGeneratorInterface $urlGenerator, HydraService $hydra
private HydraService $hydra
) { ) {
$this->hydra = $hydra;
} }
public static function getSubscribedEvents(): array public static function getSubscribedEvents(): array

View File

@ -3,29 +3,23 @@
namespace App\Hydra; namespace App\Hydra;
use App\Hydra\Exception\InvalidChallengeException; use App\Hydra\Exception\InvalidChallengeException;
use App\Services\PdoService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class HydraService extends AbstractController class HydraService extends AbstractController
{ {
public SessionInterface $session; public SessionInterface $session;
public UrlGeneratorInterface $router;
public Client $client; public Client $client;
public PdoService $pdoServices;
public TokenStorageInterface $tokenStorage; public TokenStorageInterface $tokenStorage;
public function __construct(PdoService $pdoServices, Client $client, SessionInterface $session, UrlGeneratorInterface $router, TokenStorageInterface $tokenStorage) public function __construct(Client $client, SessionInterface $session, TokenStorageInterface $tokenStorage)
{ {
$this->pdoServices = $pdoServices;
$this->session = $session; $this->session = $session;
$this->client = $client; $this->client = $client;
$this->router = $router;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
} }

View File

@ -2,7 +2,7 @@
namespace App; namespace App;
use App\DependencyInjection\PdoExtension; use App\DependencyInjection\SQLLoginExtension;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -22,10 +22,10 @@ class Kernel extends BaseKernel
$this->microKernelConfigureContainer($loader); $this->microKernelConfigureContainer($loader);
$loader->load(function (ContainerBuilder $container) use ($loader) { $loader->load(function (ContainerBuilder $container) use ($loader) {
$envLanguage = \getenv('APP_LOCALES'); $envLanguage = getenv('APP_LOCALES');
$container->setParameter('app.supported_locales', explode(',', $envLanguage)); $container->setParameter('app.supported_locales', explode(',', $envLanguage));
$container->registerExtension(new PdoExtension()); $container->registerExtension(new SQLLoginExtension());
$loader->load($this->getConfigDir().'/pdo_configuration/*.{yml,yaml}', 'glob'); $loader->load($this->getConfigDir().'/sql_login_configuration/*.{yml,yaml}', 'glob');
}); });
} }
} }

View File

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

View File

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

View File

@ -1,66 +0,0 @@
<?php
namespace App\Pdo;
class PdoRequest
{
public const DATA_TO_FETCH = 'data_to_fetch';
public const COLUMN_LOGIN_NAME = 'column_login_name';
public const COLUMN_PASSWORD_NAME = 'column_password_name';
public const TABLE_NAME = 'table_name';
protected array $config;
protected string $dsn;
protected string $user;
protected string $password;
public function __construct(array $config = [], string $dsn, string $user, string $password)
{
$this->config = $config;
$this->dsn = $dsn;
$this->user = $user;
$this->password = $password;
}
public function getDatabaseDsn()
{
return $this->dsn;
}
public function getDbUser()
{
return $this->user;
}
public function getDbPassword()
{
return $this->password;
}
public function getNameLogin()
{
return $this->config[self::COLUMN_LOGIN_NAME];
}
public function getNamePassword()
{
return $this->config[self::COLUMN_PASSWORD_NAME];
}
public function getRequestScope()
{
$scope = '';
foreach ($this->config[self::DATA_TO_FETCH] as $data) {
$scope .= $data.',';
}
$scope = substr($scope, 0, -1);
$request = 'SELECT '.$scope.' FROM '.$this->config[self::TABLE_NAME].' WHERE '.$this->config[self::COLUMN_LOGIN_NAME].' = :'.$this->config[self::COLUMN_LOGIN_NAME].';';
return $request;
}
public function getRequestLogin()
{
return 'SELECT '.$this->config[self::COLUMN_PASSWORD_NAME].' FROM '.$this->config[self::TABLE_NAME].' WHERE '.$this->config[self::COLUMN_LOGIN_NAME].' = :'.$this->config[self::COLUMN_LOGIN_NAME].';';
}
}

View File

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

View File

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

View File

@ -1,11 +1,11 @@
<?php <?php
namespace App\Pdo; namespace App\SQLLogin;
use PDO; use PDO;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class PdoConnect extends AbstractController class SQLLoginConnect extends AbstractController
{ {
/** /**
* @var Singleton * @var Singleton
@ -35,7 +35,7 @@ class PdoConnect extends AbstractController
public static function getInstance() public static function getInstance()
{ {
if (is_null(self::$_instance)) { if (is_null(self::$_instance)) {
self::$_instance = new PdoConnect(); self::$_instance = new SQLLoginConnect();
} }
return self::$_instance; return self::$_instance;

View File

@ -0,0 +1,77 @@
<?php
namespace App\SQLLogin;
class SQLLoginRequest
{
public const DATA_TO_FETCH = 'data_to_fetch';
public const LOGIN_COLUMN_NAME = 'login_column_name';
public const SALT_COLUMN_NAME = 'salt_column_name';
public const PASSWORD_COLUMN_NAME = 'password_column_name';
public const TABLE_NAME = 'table_name';
protected array $config;
protected string $dsn;
protected string $user;
protected string $password;
public function __construct(array $config = [], string $dsn, string $user, string $password)
{
$this->config = $config;
$this->dsn = $dsn;
$this->user = $user;
$this->password = $password;
}
public function getDatabaseDsn()
{
return $this->dsn;
}
public function getDbUser()
{
return $this->user;
}
public function getDbPassword()
{
return $this->password;
}
public function getLoginColumnName()
{
return $this->config[self::LOGIN_COLUMN_NAME];
}
public function egtPasswordColumnName()
{
return $this->config[self::PASSWORD_COLUMN_NAME];
}
public function getSaltColumnName()
{
return $this->config[self::SALT_COLUMN_NAME];
}
public function getRequestScope()
{
$scope = '';
foreach ($this->config[self::DATA_TO_FETCH] as $data) {
$scope .= $data.',';
}
$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;
}
public function getRequestPassword()
{
$passwordColumns = $this->config[self::PASSWORD_COLUMN_NAME];
if (!empty($this->config[self::SALT_COLUMN_NAME])) {
$passwordColumns .= ', '.$this->config[self::SALT_COLUMN_NAME];
}
return 'SELECT '.$passwordColumns.' FROM '.$this->config[self::TABLE_NAME].' WHERE '.$this->config[self::LOGIN_COLUMN_NAME].' = :'.$this->config[self::LOGIN_COLUMN_NAME].';';
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Security\Hasher;
use App\SQLLogin\Exception\InvalidSQLPasswordException;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
class PasswordEncoder implements LegacyPasswordHasherInterface
{
use CheckPasswordLengthTrait;
protected ?string $pepper;
protected string $hashAlgo;
public function __construct(?string $pepper, string $hashAlgo)
{
$this->pepper = $pepper;
$this->hashAlgo = $hashAlgo;
}
public function hash(string $plainPassword, string $salt = null): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
$hash = hash($this->hashAlgo, $plainPassword.$salt.$this->pepper);
rmasson marked this conversation as resolved Outdated

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.

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

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
return $hash;
}
public function verify(string $hashedPassword, string $plainPassword, string $salt = null): bool
{
if ('' === $plainPassword || $this->isPasswordTooLong($plainPassword)) {
return false;
}
if ($this->hash($plainPassword, $salt) === $hashedPassword) {

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.

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
return true;
} else {
throw new InvalidSQLPasswordException();
}
}
public function needsRehash(string $hashedPassword): bool
{
return false;
}
}

View File

@ -3,8 +3,9 @@
namespace App\Security; namespace App\Security;
use App\Entity\User; use App\Entity\User;
use App\Pdo\Exception\InvalidPasswordException; use App\Security\Hasher\PasswordEncoder;
use App\Services\PdoService; use App\Service\SQLLoginService;
use App\SQLLogin\Exception\InvalidSQLPasswordException;
use PDOException; use PDOException;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -19,7 +20,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class PdoUserAuthenticator extends AbstractAuthenticator 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';
@ -27,14 +28,16 @@ class PdoUserAuthenticator extends AbstractAuthenticator
public const ERROR_PDO = 'error_pdo'; public const ERROR_PDO = 'error_pdo';
protected string $baseUrl; protected string $baseUrl;
private PdoService $pdoService; private SQLLoginService $pdoService;

Je pense que la variable n'a pas été renommée $pdoService en accord avec le changement de nom du service.

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;
public function __construct(string $baseUrl, PdoService $pdoService, UrlGeneratorInterface $router) public function __construct(string $baseUrl, SQLLoginService $pdoService, UrlGeneratorInterface $router, PasswordEncoder $passwordHasher)
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->pdoService = $pdoService;
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;
} }
/** /**
@ -65,20 +68,22 @@ class PdoUserAuthenticator extends AbstractAuthenticator
{ {
$form = $request->request->get('login'); $form = $request->request->get('login');
$login = $form['login']; $login = $form['login'];
$password = $form['password']; $plaintextPassword = $form['password'];
$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
$remoteHashedPassword = $this->pdoService->fetchPassword($login); list($remoteHashedPassword, $remoteSalt) = $this->pdoService->fetchPassword($login);
} catch (PDOException $e) { } catch (PDOException $e) {
$request->getSession()->set(self::ERROR_PDO, true); $request->getSession()->set(self::ERROR_PDO, true);
throw new AuthenticationException(); throw new AuthenticationException();
} }
if ($remoteHashedPassword) { if ($remoteHashedPassword) {
try { try {
$this->pdoService->verifyPassword($password, $remoteHashedPassword); // Comparaison remote hash et hash du input password + salt
$this->passwordHasher->verify($remoteHashedPassword, $plaintextPassword, $remoteSalt);
$attributes = $this->pdoService->fetchDatas($login); $attributes = $this->pdoService->fetchDatas($login);
$user = new User($login, $password, $attributes, $rememberMe); $user = new User($login, $remoteHashedPassword, $attributes, $rememberMe);
$loader = function (string $userIdentifier) use ($user) { $loader = function (string $userIdentifier) use ($user) {
return $user->getLogin() == $userIdentifier ? $user : null; return $user->getLogin() == $userIdentifier ? $user : null;
}; };
@ -89,7 +94,7 @@ class PdoUserAuthenticator extends AbstractAuthenticator
$passport->setAttribute('attributes', $user->getAttributes()); $passport->setAttribute('attributes', $user->getAttributes());
return $passport; return $passport;
} catch (InvalidPasswordException $e) { } catch (InvalidSQLPasswordException $e) {
$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) {

View File

@ -9,7 +9,7 @@ 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;
class PdoUserProvider implements UserProviderInterface class SQLLoginUserProvider implements UserProviderInterface
{ {
protected RequestStack $requestStack; protected RequestStack $requestStack;

View File

@ -1,25 +1,23 @@
<?php <?php
namespace App\Services; namespace App\Service;
use App\SQLLogin\SQLLoginConnect;
use App\SQLLogin\SQLLoginRequest;
use PDO; use PDO;
use PDOException; use PDOException;
use App\Pdo\PdoConnect;
use App\Pdo\PdoRequest;
use Sentry\captureException;
use App\Pdo\Exception\InvalidPasswordException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class PdoService extends AbstractController class SQLLoginService extends AbstractController
{ {
private $params; private $params;
public PdoRequest $pdoRequest; public SQLLoginRequest $sqlLoginRequest;
public function __construct(ParameterBagInterface $params, PdoRequest $pdoRequest) public function __construct(ParameterBagInterface $params, SQLLoginRequest $sqlLoginRequest)
{ {
$this->params = $params; $this->params = $params;
$this->pdoRequest = $pdoRequest; $this->sqlLoginRequest = $sqlLoginRequest;
} }
public function fetchDatas($login) public function fetchDatas($login)
@ -28,11 +26,11 @@ class PdoService extends AbstractController
$dbh = $this->getConnection(); $dbh = $this->getConnection();
// forge de la requête // forge de la requête
$request = $this->pdoRequest->getRequestScope(); $request = $this->sqlLoginRequest->getRequestScope();
// Préparation de la requête // Préparation de la requête
$query = $dbh->prepare($request); $query = $dbh->prepare($request);
$query->execute([$this->pdoRequest->getNameLogin() => $login]); $query->execute([$this->sqlLoginRequest->getLoginColumnName() => $login]);
$datas = $query->fetch(PDO::FETCH_ASSOC); $datas = $query->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) { } catch (PDOException $e) {
\Sentry\captureException($e); \Sentry\captureException($e);
@ -52,9 +50,9 @@ class PdoService extends AbstractController
{ {
try { try {
$dbh = $this->getConnection(); $dbh = $this->getConnection();
$request = $this->pdoRequest->getRequestLogin(); $request = $this->sqlLoginRequest->getRequestPassword();
$query = $dbh->prepare($request); $query = $dbh->prepare($request);
$query->execute([$this->pdoRequest->getNameLogin() => $login]); $query->execute([$this->sqlLoginRequest->getLoginColumnName() => $login]);
$password = $query->fetch(PDO::FETCH_ASSOC); $password = $query->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) { } catch (PDOException $e) {
\Sentry\captureException($e); \Sentry\captureException($e);
@ -62,7 +60,10 @@ class PdoService extends AbstractController
throw new PDOException(); throw new PDOException();
} }
if ($password) { if ($password) {
return $password[$this->pdoRequest->getNamePassword()]; return [
$password[$this->sqlLoginRequest->egtPasswordColumnName()],
isset($password[$this->sqlLoginRequest->getSaltColumnName()]) ? $password[$this->sqlLoginRequest->getSaltColumnName()] : null,
];
} }
return false; return false;
@ -71,19 +72,8 @@ class PdoService extends AbstractController
public function getConnection() public function getConnection()
{ {
// Appel du singleton // Appel du singleton
$pdo = PdoConnect::getInstance(); $pdo = SQLLoginConnect::getInstance();
Idem, cf. [commentaire précédent](https://forge.cadoles.com/Cadoles/hydra-sql/pulls/3/files#issuecomment-55979)
// Connection bdd // Connection bdd
return $pdo->connect($this->pdoRequest->getDatabaseDsn(), $this->pdoRequest->getDbUser(), $this->pdoRequest->getDbPassword()); return $pdo->connect($this->sqlLoginRequest->getDatabaseDsn(), $this->sqlLoginRequest->getDbUser(), $this->sqlLoginRequest->getDbPassword());
}
public function verifyPassword($password, $hashedPassword)
{
$hashAlgo = $this->params->get('hashAlgo') ?? 'sha256';
if ($hashedPassword === hash($hashAlgo, $password)) {
return true;
} else {
throw new InvalidPasswordException();
}
} }
} }

View File

@ -1 +1 @@
123 134
Review

Ce fichier doit il vraiment être commit dans le dépôt ? Attention avec les git add -A :P

Ce fichier doit il vraiment être commit dans le dépôt ? Attention avec les `git add -A` :P