TD8 – Authentification & validation par email Sécurité des mots de passe
Ce TD vient à la suite du TD7 – cookies & sessions et prendra donc comme acquis l’utilisation des cookies et des sessions. Dans ce TD, nous allons :
- mettre en place l’authentification par mot de passe des utilisateurs du site ;
- mettre en place une validation par email de l’inscription des utilisateurs ;
- verrouiller l’accès à certaines pages ou actions à certains utilisateurs. Par exemple, un utilisateur ne peut modifier que ses données. Ou encore, l’administrateur a tous les droits sur les utilisateurs.
Authentification par mot de passe
Stockage sécurisé de mot de passe
Nous allons stocker le mot de passe d’un utilisateur dans la base de données. Cependant, on ne stocke jamais le mot de passe en clair (de manière directement lisible) pour plusieurs raisons :
- l’utilisateur souhaite que personne ne connaisse son mot de passe, y compris l’administrateur du site Web. C’est une règle de la CNIL (Commission nationale de l’informatique et des libertés) qui veille à la protection des données personnelles.
- Un attaquant qui arriverait à se connecter à la base de données apprendrait directement tous les mots de passe.
Idée 1 : Chiffrement
À la réception du mot de passe, le serveur pourrait le chiffrer et stocker ce chiffré dans la base de données.
Problème : L’administrateur du site pourrait toujours lire les mots de passe. En effet, il peut lire la clé secrète sur le serveur et peut donc déchiffrer les mots de passe.
Idée 2 : Hachage
Utilisons une fonction de hachage cryptographique, c’est-à-dire une fonction qui vérifie notamment les propriétés suivantes (source : Wikipedia):
- la valeur de hachage d’un message se calcule « facilement » ;
- il est extrêmement difficile, pour une valeur de hachage donnée, de construire un message ayant cette valeur (résistance à la préimage) ;
Ainsi, si un site Web stocke les mots de passe hachés dans sa base de données, l’administrateur du site ne pourra pas lire ces mots de passes.
$mdpClair = 'apple';
echo hash('sha256', $mdpClair); // SHA-256 est un algorithme de hachage
// Affiche '3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b'
(Une manière simple d’exécuter ce code est d’ouvrir un interpréteur PHP
interactif en exécutant php -a dans le terminal. Il suffit alors de
couper/coller le code PHP dans l’interpréteur.)
Cependant, le site peut quand même vérifier un mot de passe
$mdpClair = 'apple';
$mdpHache = '3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b';
var_dump($mdpHache == hash('sha256', $mdpClair));
// Renvoie true
Problème :
- L’administrateur du site peut facilement voir si deux utilisateurs ont le même mot de passe.
- Rainbow table : Rapidement, c’est une structure de données qui permet de retrouver des mots de passe avec un bon compromis stockage/temps. Cette technique est surtout utile pour essayer d’attaquer de nombreux mots de passes à la fois, par exemple tous ceux des utilisateurs d’un site Web.
- Si le mot de passe est trop commun (par exemple, un mot du dictionnaire), il est très facile de le déchiffrer à l’aide un site dédié (prochain exercice).
Créez à partir du code précédent le haché Sha-256 d’un mot du dictionnaire
français (placez par exemple le code dans un fichier PHP temporaire et accédez-y
via le navigateur).
Utilisez un site comme dcode
pour retrouver le mot de passe originel à partir de son haché.
Explication : Ce site annonce stocke le haché de mots de passe communs.
Si votre mot de passe est l’un de ceux-là, sa sécurité est compromise.
Heureusement, il existe beaucoup plus de mot de passe possible ! Par exemple,
rien qu’en utilisant des mots de passe de longueur 10 écrits à partir des 64
caractères 0,1,...,9,A,B,...,Z,a,...,z,+,/, vous avez (64)^10 = 2^60 ≃ 10^18
possibilités.
Idée 3 : Saler et hacher
Comme une rainbow table est dépendante d’un algorithme de hachage, nous allons hacher différemment chaque mot de passe. Pour ceci, nous allons concaténer une chaîne aléatoire, appelée sel, au début de chaque mot de passe avant de le hacher.
Dans le scénario où deux utilisateurs ont un même mot de passe, l’utilisation d’un sel aléatoire, donc différent, permet que leurs mots de passe salés/hachés respectifs soient différents.
La base de données doit stocker un sel et un haché pour chaque mot de passe. En effet, la connaissance du sel est nécessaire pour tester un mot de passe.
Nous allons utiliser l’implémentation suivante de PHP de la fonction de hachage
bcrypt qui a la particularité d’intégrer automatiquement un sel aléatoire.
Ainsi, nous n’aurons besoin d’ajouter qu’un seul champ à notre BDD qui
contiendra à la fois le sel et le haché.
$mdpClair = 'apple';
// PASSWORD_DEFAULT est une constante PHP qui permet de spécifier l'algorithme
// utilisé par défaut dans le hachage : actuellement c'est l'algorithme bcrypt
var_dump(password_hash($mdpClair, PASSWORD_DEFAULT));
// Le hachage d'un même mot de passe donne des résultats différents
var_dump(password_hash($mdpClair, PASSWORD_DEFAULT));
Le code précédent affiche par exemple :
$2y$10$VZxpwQN8.vVc5UkJy.dBh.n2yRC4Uh9dqrHxvyC.SlSlyDaZKPzQW
La sortie contient plusieurs informations (source : Wikipedia):
2y:2correspond à l’algorithme de hachage, icibcrypt,ycorrespond à la version de l’algorithme
10: coût de l’algorithme. Augmenter le coût de 1 double le temps de calcul de la fonction de hachage. Ceci est utile pour limiter les capacités de l’attaque par force brute sachant que les ordinateurs sont de plus en plus rapides.VZxpwQN8.vVc5UkJy.dBh.: Les 22 premiers caractères correspondent au sel aléatoire.n2yRC4Uh9dqrHxvyC.SlSlyDaZKPzQW: Les 31 caractères finaux correspondent au haché
On peut vérifier qu’un mot de passe en clair correspond bien à un mot de passe haché :
$mdpClair = 'apple';
$mdpHache1 = password_hash($mdpClair, PASSWORD_DEFAULT);
$mdpHache2 = password_hash($mdpClair, PASSWORD_DEFAULT);
var_dump(password_verify($mdpClair, $mdpHache1)); // True
var_dump(password_verify($mdpClair, $mdpHache2)); // True
L’utilisation d’un sel résout le problème “L’administrateur peut voir si deux utilisateurs ont le même mot de passe”. En effet, les hachés de deux utilisateurs ayant le même mot de passe sont différents parce qu’ils utilisent des sels différents (car tirés au hasard).
Problème :
- Si un attaquant arrive à lire la base de données (en utilisant une injection
SQL par exemple), il peut toujours effectuer les attaques suivantes sur les
mots de passe hachés :
- attaque par force brute : L’attaquant essaye tous les mots de passes possibles en commençant par ceux de petite taille.
- attaque par dictionnaire : L’attaquant essaye les mots de passes les plus courants, par exemple les mots du dictionnaire, ou en trouvant une liste des mots de passe les plus communs.
Idée 4 : Poivrer, saler et hacher
L’idée finale est de rajouter une autre chaîne aléatoire, appelée poivre, dans le hachage du mot de passe. La particularité du poivre est qu’il ne doit pas être stocké dans la base de données. Ainsi, si la base de données est compromise, l’attaquant n’apprend rien sur les mots de passe, car il ne connaît pas le poivre. En effet, le poivre est nécessaire pour tester un mot de passe.
En pratique, nous stockerons un unique poivre par site, qui est choisi aléatoirement :
$poivre = "M7UKGv9fkptxwbSmZvlr1U";
// Le client envoie son mot de passe en clair au serveur
$mdpClair = 'apple';
// Le serveur hache le mot de passe une première fois puis le transforme à l'aide d'un secret appelé poivre
$mdpPoivre = hash_hmac("sha256", $mdpClair, $poivre);
// Le mot de passe poivré peut alors être salé/haché avec l'algorithme BCRYPT avant d'être enregistré en BDD
$mdpHache = password_hash($mdpPoivre, PASSWORD_DEFAULT);
var_dump($mdpHache);
Explication : Dans l’esprit, la fonction hash_hmac permet d’appliquer un
salage/hachage en spécifiant le sel. Nous l’utiliserons en spécifiant le sel
$poivre. En effet, le poivre joue le même rôle qu’un sel, sauf qu’il n’est pas
stocké en BD et qu’il est unique au site (il ne change pas à chaque hachage de
mot de passe).
Mise en place de la BDD et des formulaires
On vous donne la classe MotDePasse qui reprend les explications précédentes.
Prenez le temps de bien comprendre les deux fonctions hacher et verifier.
namespace App\Covoiturage\Lib;
class MotDePasse
{
// Exécutez genererChaineAleatoire() et stockez sa sortie dans le poivre
private static string $poivre = "";
public static function hacher(string $mdpClair): string
{
$mdpPoivre = hash_hmac("sha256", $mdpClair, MotDePasse::$poivre);
$mdpHache = password_hash($mdpPoivre, PASSWORD_DEFAULT);
return $mdpHache;
}
public static function verifier(string $mdpClair, string $mdpHache): bool
{
$mdpPoivre = hash_hmac("sha256", $mdpClair, MotDePasse::$poivre);
return password_verify($mdpPoivre, $mdpHache);
}
public static function genererChaineAleatoire(int $nbCaracteres = 22): string
{
// 22 caractères par défaut pour avoir au moins 128 bits aléatoires
// 1 caractère = 6 bits car 64=2^6 caractères en base_64
// et 128 <= 22*6 = 132
$octetsAleatoires = random_bytes(ceil($nbCaracteres * 6 / 8));
return substr(base64_encode($octetsAleatoires), 0, $nbCaracteres);
}
}
// Pour créer votre poivre (une seule fois)
// var_dump(MotDePasse::genererChaineAleatoire());
-
Copiez/collez dans un nouveau dossier
TD8tous les fichiers du dossierTD7. -
Copiez le code de la classe présentée au-dessus dans le fichier
src/Lib/MotDePasse.php. -
Ouvrez un terminal dans votre conteneur Docker : dans Docker Desktop, ouvrez votre conteneur, puis cliquez sur l’onglet
Exec. Dans le terminal qui apparaît, tapezbashpour retrouver votre shell habituel. Via ce terminal, rendez-vous dans le dossiertds-php/TD8/src/Lib. -
Décommentez la dernière ligne du fichier
MotDePasse.php, puis, dans le terminal (sous Docker), exécutez ce fichier :php MotDePasse.phpPuis copiez le résultat dans l’attribut statique
$poivreune fois pour toutes. -
Nous allons modifier la structure de données utilisateur :
- Modifiez la table utilisateur en lui ajoutant une colonne
VARCHAR mdpHache (taille 256)nonnullstockant son mot de passe. - Mettez à jour la classe métier
Utilisateur(dossiersrc/Modele/DataObject) :- ajoutez un attribut
private string $mdpHache, - mettez à jour le constructeur,
- rajoutez un getter et un setter.
- ajoutez un attribut
- Mettez à jour la classe de persistance
UtilisateurRepository:- mettez à jour
getNomsColonnes, - mettez à jour
construireDepuisTableauSQL(qui permet de construire un utilisateur à partir de la sortie d’une requête SQL), - mettez à jour la méthode
formatTableauSQL(qui fournit les données des requêtes SQL préparées).
- mettez à jour
Note : L’utilisation d’un framework PHP professionnel nous éviterait ces tâches répétitives.
- Modifiez la table utilisateur en lui ajoutant une colonne
Nous allons modifier la création d’un utilisateur.
- Modifier la vue
utilisateur/formulaireCreation.phppour ajouter deux champs password au formulaire<p class="InputAddOn"> <label class="InputAddOn-item" for="mdp_id">Mot de passe*</label> <input class="InputAddOn-field" type="password" value="" placeholder="" name="mdp" id="mdp_id" required> </p> <p class="InputAddOn"> <label class="InputAddOn-item" for="mdp2_id">Vérification du mot de passe*</label> <input class="InputAddOn-field" type="password" value="" placeholder="" name="mdp2" id="mdp2_id" required> </p>Le deuxième champ mot de passe sert à valider le premier.
- Modifiez l’action
creerDepuisFormulairedu utilisateur :-
rajoutez la condition que les deux champs mot de passe doivent coïncider avant de sauvegarder l’utilisateur. En cas d’échec, appelez à l’action d’erreur
afficherErreuravec un message Mots de passe distincts. -
Modifiez la méthode
ControleurUtilisateur::construireDepuisFormulairequi construit un objet métier utilisateur à partir d’un tableau$tableauDonneesFormulaire(voir fin du TD6) pour qu’elle appelle le constructeur deUtilisateuren hachant d’abord le mot de passe.
-
-
Rajoutons au menu de notre site un lien pour s’inscrire. Dans le menu de la vue générique
vueGenerale.php, rajoutez une icône cliquable
1 qui pointe vers l’action
afficherFormulaireCreation(contrôleur utilisateur). - Testez l’inscription d’un utilisateur avec mot de passe (vérifiez la ligne correspondant au mot de passe dans la base de données).
Rajoutons des mots de passe dans la mise à jour d’un utilisateur.
- Modifier la vue
formulaireMiseAJour.phppour ajouter trois champs password : l’ancien mot de passe, le nouveau qu’il faut écrire 2 fois pour ne pas se tromper. -
Testez la mise à jour du mot de passe d’un utilisateur (qui doit marcher car elle appelle
construireDepuisFormulaireque nous avons mis à jour).
Note : Nous ferons prochainement les vérifications de l’ancien mot de passe, de l’égalité des 2 nouveaux mots de passe.
Sécurisation d’une page avec les sessions
Pour accéder à une page réservée, un utilisateur doit s’authentifier. Une fois authentifié, un utilisateur peut accéder à toutes les pages réservées sans avoir à retaper son mot de passe. Il faut donc faire circuler l’information “s’être authentifié” de pages en pages : nous allons donc utiliser les sessions.
Connexion d’un utilisateur
Procédons en plusieurs étapes :
-
Nous allons regrouper les méthodes liées à la connexion d’un utilisateur dans la classe purement statique
ConnexionUtilisateur. Créez cette classe dans le fichiersrc/Lib/ConnexionUtilisateur.phpà partir du code suivant et complétez-la pour que :- La connexion enregistre le login d’un utilisateur en session dans le champ
$cleConnexion. - Le client est connecté si et seulement si la session contient un enregistrement associé à la clé
$cleConnexion. - La déconnexion consiste à supprimer cet enregistrement de la session.
getLoginUtilisateurConnecte()renvoienullsi le client n’est pas connecté.
namespace App\Covoiturage\Lib; class ConnexionUtilisateur { // L'utilisateur connecté sera enregistré en session associé à la clé suivante private static string $cleConnexion = "_utilisateurConnecte"; public static function connecter(string $loginUtilisateur): void { // À compléter } public static function estConnecte(): bool { // À compléter } public static function deconnecter(): void { // À compléter } public static function getLoginUtilisateurConnecte(): ?string { // À compléter } } - La connexion enregistre le login d’un utilisateur en session dans le champ
-
Rajoutons au menu de notre site un lien pour se connecter. Dans le menu de la vue générique
vueGenerale.php, rajoutez une icône cliquable
qui pointe vers la future
action afficherFormulaireConnexion(contrôleur utilisateur).
Ce lien, ainsi que le lien d’inscription
, ne doivent s’afficher que si aucun
utilisateur n’est connecté (utiliser une méthode de la classe
ConnexionUtilisateur).Note : Il est autorisé de mettre un
ifdans la vuevueGenerale.php. -
Créons une vue pour afficher un formulaire de connexion :
- Créer une vue
utilisateur/formulaireConnexion.phpqui comprend un formulaire avec deux champs, l’un pour le login, l’autre pour le mot de passe. Ce formulaire appelle la future actionconnecterdu contrôleur utilisateur. - Ajouter une action
afficherFormulaireConnexionqui affiche ce formulaire.
- Créer une vue
- Créons enfin l’action
connecter()du contrôleur utilisateur :- Commençons par les vérifications à faire avant de se connecter. La
première vérification est qu’un login et un mot de passe sont transmis dans le
query string. Sinon, appelez
afficherErreuravec le message Login et/ou mot de passe manquant. - Puis, il faut récupérer l’utilisateur ayant le login transmis. Ceci
permettra de vérifier que ce login existe bien et que le mot de passe
transmis est correct (utiliser une méthode de la classe
MotDePasse). Sinon, appelezafficherErreuravec le message Login et/ou mot de passe incorrect. - Enfin, vous pouvez connecter l’utilisateur (utiliser une méthode de la
classe
ConnexionUtilisateur). Affichez une nouvelle vueutilisateur\utilisateurConnecte.phpqui écrit un message Utilisateur connecté puis appelle la vuedetail.phppour afficher les informations de l’utilisateur connecté.
- Commençons par les vérifications à faire avant de se connecter. La
première vérification est qu’un login et un mot de passe sont transmis dans le
query string. Sinon, appelez
- Tentez de vous connecter et vérifiez que le menu de navigation s’affiche correctement (sans les boutons de connexion et d’inscription).
Codons maintenant la déconnexion.
- Ajoutez au menu de navigation de la vue générique
vueGenerale.phpdeux icônes, seulement visibles quand l’utilisateur est connecté :- la première contient une icône cliquable
qui renvoie vers la vue de
détail de l’utilisateur connecté. - puis une deuxième case avec une icône cliquable
qui pointe vers la
future action deconnecter(contrôleur utilisateur).
- la première contient une icône cliquable
-
Ajouter une action
deconnecterqui déconnecte l’utilisateur (utiliser une méthode de la classeConnexionUtilisateur). Affichez une nouvelle vueutilisateur\utilisateurDeconnecte.phpqui affiche le message Utilisateur déconnecté puis la liste des utilisateurs.Note : Toutes les vues
utilisateurConnecte.php,utilisateurDeconnecte.php,utilisateurCree.php,utilisateurMisAJour.phpetutilisateurSupprime.phpsont bien sûr redondantes. Nous résoudrons ce problème lors du TD9 sur les messages Flash. -
Testez qu’un clic sur vos deux nouvelles icônes
et
marche bien.Question innocente 👼 : Est-ce que le clic sur
pour un utilisateur de login &a=bmarche bien ?
Sécurisation d’une page à accès réservé
On souhaite restreindre les actions de mise à jour et de suppression à l’utilisateur actuellement authentifié. Commençons par limiter les liens.
-
Faites en sorte que la vue
utilisateur/liste.phpn’affiche que les liens vers la vue de détail des utilisateurs, mais pas les liens de modification ou de suppression (vous pouvez néanmoins garder le code correspondant quelque part, car nous nous en resservirons plus tard.). -
Modifier la vue de détail pour qu’elle affiche les liens vers la mise à jour ou la suppression de l’utilisateur seulement si le login de l’utilisateur concorde avec celui stocké en session.
Pour vous aider dans cette tâche, rajoutez la méthode suivante à
ConnexionUtilisateur:public static function estUtilisateur($login): boolqui doit vérifier si un utilisateur est connecté et que son login correspond à celui passé en argument de la fonction.
Attention : Supprimer le lien n’est pas suffisant, car un petit malin
pourrait accéder au formulaire de mise à jour d’un utilisateur quelconque en
rentrant manuellement l’action afficherFormulaireMiseAJour dans l’URL.
-
« Hackez » votre site en accédant à la page de mise à jour d’un utilisateur quelconque (qui n’est pas vous) en manipulant le query string.
-
Modifiez l’action
afficherFormulaireMiseAJourdu contrôleur utilisateur de sorte que l’accès au formulaire soit restreint à l’utilisateur connecté. En cas de problème, utiliserafficherErreurpour afficher un message La mise à jour n’est possible que pour l’utilisateur connecté.Note : la succession des
if,else,ifpourrait être évité en utilisant desreturn;dans chaque cas d’erreur. Ce style de codage est plus sûr, car on sait plus facilement dans quel cas on est. Par exemple :if(!isset($_GET["attribut"])) { //Cas d'erreur 1 self::afficherErreur("..."); return; } if(!Service::verification()) { //Cas d'erreur 2 self::afficherErreur("..."); return; } //Traitement normal -
Vérifiez qu’il n’est plus possible d’accèder à la page de mise à jour d’un autre utilisateur.
Attention : Restreindre l’accès au formulaire de mise à jour n’est toujours pas
suffisant car un petit malin pourrait exécuter une mise à jour en demandant manuellement
l’action mettreAJour.
-
« Hackez » votre site en effectuant une mise à jour d’un utilisateur quelconque sans changer de code PHP2. Note : Ce « hack » sera bien plus simple à réaliser si le formulaire de mise à jour est en méthode
GET, et pareil pour sa page de traitement. - Mettez à jour l’action
mettreAJourdu contrôleur utilisateur pour qu’il effectue toutes les vérifications suivantes, avecafficherErreuren cas de problème :- vérifiez que tous les champs obligatoires du formulaire ont été transmis ;
- Vérifiez que le login existe ;
- Vérifiez que les 2 nouveaux mots de passe coïncident ;
- Vérifiez que l’ancien mot de passe est correct ;
- Vérifiez que l’utilisateur mis-à-jour correspond à l’utilisateur connecté.
-
Sécurisez de manière similaire l’accès à l’action
supprimerd’un utilisateur. - Vérifiez qu’il n’est plus possible de “hacker” le site comme dans la première question.
(clic droit puis inspecter un élément, ou bien F12) car cette manipulation
se fait du côté client.
Note générale importante : les seules pages qu’il est vital de sécuriser
sont celles dont le script effectue vraiment l’action de mise à jour ou de
suppression, c.-à-d. les actions mettreAJour et supprimer. Les autres sécurisations
sont surtout pour améliorer l’ergonomie du site.
De manière générale, il ne faut jamais faire confiance au client ; seule une
vérification côté serveur est sûre.
Rôle administrateur
Jusqu’au début de ce TD, le site était codé comme si tout le monde avait le rôle d’administrateur. Maintenant, nous allons différencier ceux qui ont ce rôle des autres utilisateurs. Nous souhaitons donc pouvoir avoir des comptes administrateur sur notre site.
Commençons par rajouter un attribut estAdmin à notre classe métier
Utilisateur et à son stockage UtilisateurRepository.
-
Ajouter un champ
estAdminde typeBOOLEAN(ouTINYINT(1)) nonNULLà la tableutilisateur. - Mettez à jour la classe métier
Utilisateur(dossiersrc/Modele/DataObject) :- ajoutez un attribut
private bool $estAdmin, - mettez à jour le constructeur,
- rajoutez un getter et un setter,
- ajoutez un attribut
- Mettez à jour la classe de persistance
UtilisateurRepository:- mettez à jour
getNomsColonnes, -
mettez à jour la méthode
formatTableauSQL(qui fournit les données des requêtes SQL préparées).Rappel : SQL stocke différemment les booléens que PHP (cf.
nonFumeurdes trajets). En SQL, on encodefalseavec l’entier0ettrueavec l’entier1. Il faut donc que votre méthodeformatTableauSQLrenvoie0ou1pour le champestAdminTag. -
mettez à jour
construireDepuisTableauSQL(qui permet de construire un utilisateur à partir de la sortie d’une requête SQL).Note : Pas besoin ici de convertir le booléen SQL (0 ou 1) vers un booléen PHP car PHP le fait automatiquement.
- mettez à jour
Rôle administrateur lors de la création d’un utilisateur
Modifions le processus de création d’un utilisateur pour intégrer cette nouvelle donnée.
- Rajoutez un bouton
checkboxau formulaire de création<p class="InputAddOn"> <label class="InputAddOn-item" for="estAdmin_id">Administrateur</label> <input class="InputAddOn-field" type="checkbox" placeholder="" name="estAdmin" id="estAdmin_id"> </p> -
Mettez à jour
construireDepuisFormulairedeControleurUtilisateur.Rappel : Les formulaires transmettent le booléen associé à une
checkboxde manière spécifique (cf.nonFumeurdes trajets). Si la case est cochée, alorsestAdmin=onsera transmis. Si la case n’est pas cochée, aucune donnée n’est transmise (on vérifie donc avec la fonctionisset). - Testez de créer des utilisateurs administrateurs puis vérifiez dans PHPMyAdmin que la colonne
estAdminvaut bien 1 (true) pour ces utilisateurs.
Rôle administrateur lors de la mise à jour d’un utilisateur
Passons au processus de mise-à-jour.
- Rajoutez un bouton
checkboxau formulaire de mise-à-jour<p class="InputAddOn"> <label class="InputAddOn-item" for="estAdmin_id">Administrateur</label> <input class="InputAddOn-field" type="checkbox" placeholder="" name="estAdmin" id="estAdmin_id"> </p>Faites en sorte que le bouton soit pré-coché (attribut
checked) si l’utilisateur est déjà administrateur. - Vérifiez que la mise à jour fonctionne.
Bien sûr, nous allons bientôt renforcer la sécurité du site afin que seul un administrateur puisse donner le rôle d’administrateur à un autre utilisateur.
Sécurisation du rôle administrateur
Nous allons modifier la sécurité de notre site pour qu’un administrateur ait tous les droits.
Mettons à jour la classe utilitaire ConnexionUtilisateur.
- Rajoutez la méthode suivante à
ConnexionUtilisateurpublic static function estAdministrateur() : boolCette méthode doit renvoyer
truesi un utilisateur est connecté et qu’il est administrateur. Les informations sur l’utilisateur devront être récupérées de la base de données.Remarque optionnelle : On aurait pu coder un système qui récupère une seule fois les données de l’utilisateur connecté à partir de la base de données, et le stocke dans un attribut statique de la classe
ConnexionUtilisateur.
Nous pouvons maintenant coder la logique d’autorisation d’accès.
- Processus de création :
-
Le champ Administrateur ? du formulaire de création ne doit apparaître que si l’utilisateur connecté est administrateur.
Note : Vous pouvez mettre un
ifdans la vue. -
Plus important, l’action
creerDepuisFormulairene doit créer des administrateurs que si l’utilisateur connecté est administrateur.Aide : si l’utilisateur connecté n’est pas administrateur, forcez l’utilisateur créé à ne pas être administrateur, indépendamment de la valeur reçue par le formulaire.
-
- Processus de mise-à-jour :
- Vue
liste.php: Les liens de mise-à-jour d’un utilisateur doivent apparaître quand un administrateur est connecté (utilisezConnexionUtilisateur::estAdministrateur()). - Action
afficherFormulaireMiseAJour:- L’accès au formulaire de mise à jour d’un utilisateur est autorisé soit
si c’est l’utilisateur connecté, soit si l’utilisateur connecté est
administrateur et qu’il existe bien un utilisateur avec ce login.
En cas d’accès refusé, affichez le message d’erreur Login inconnu si un admin est connecté ou La mise à jour n’est possible que pour l’utilisateur connecté sinon. - Le champ Administrateur ? du formulaire de mise-à-jour ne doit apparaître que si l’utilisateur connecté est administrateur.
- L’accès au formulaire de mise à jour d’un utilisateur est autorisé soit
si c’est l’utilisateur connecté, soit si l’utilisateur connecté est
administrateur et qu’il existe bien un utilisateur avec ce login.
- Action
mettreAJour:- L’accès à l’action
mettreAJourd’un utilisateur est autorisé soit si c’est l’utilisateur connecté, soit si l’utilisateur connecté est administrateur et qu’il existe bien un utilisateur avec ce login.
En cas d’accès refusé, affichez le message d’erreur Login inconnu si un admin est connecté ou La mise à jour n’est possible que pour l’utilisateur connecté sinon. - On ne vérifie pas l’ancien mot de passe si un admin est connecté.
-
Plus important, l’action
mettreAJourne doit modifier le rôle administrateur que si l’utilisateur connecté est administrateur.
Pour appliquer cette règle, nous allons changer la manière dont nous créons l’objet utilisateur modifié. Plutôt que de le construire à partir des données du formulaire, nous allons récupérer l’utilisateur courant de la base de donnée puis le modifier avec des mutateurs (setters). Cette façon de faire facilite les logiques plus complexes, comme modifier l’attributestAdminsous condition, et plus tard la validation de l’adresse email.Modifiez donc l’action
mettreAJourpour appeler des mutateurs plutôt queconstruireDepuisFormulaire. N’oubliez pas de hacher le mot de passe avant de le modifier. Changez le statut administrateur que si l’utilisateur connecté est administrateur (faites attention à la manière de lire la case à cocher du formulaire).
- L’accès à l’action
- Vue
- Processus de suppression :
- Vue
liste.php: Les liens de suppression d’un utilisateur doivent apparaître quand un administrateur est connecté. - Action
supprimer:- L’accès à l’action
supprimerd’un utilisateur est autorisé soit si c’est l’utilisateur connecté, soit si l’utilisateur connecté est administrateur et qu’il existe bien un utilisateur avec ce login.
En cas d’accès refusé, affichez le message d’erreur Login inconnu si un admin est connecté ou La suppression n’est possible que pour l’utilisateur connecté sinon.
- L’accès à l’action
- Vue
- Vérifiez que tout fonctionne comme attendu. Vérifiez notamment que l’administrateur peut bien réaliser toutes les actions (et qu’il voit bien les liens de mise à jour et de suppression sur la page listant les utilisateurs) et qu’un utilisateur qui n’est pas administrateur ne puisse toujours pas “hacker” le site en effectuant les actions de modification et de suppression sur un autre utilisateur que lui-même.
Il est courant qu’un site Web sépare ses interfaces administrateur et utilisateur. Vous avez tous les outils pour le mettre en place si vous le souhaitez. Le défi est de limiter la duplication du code entre les 2 interfaces.
Dans un site professionnel, l’administrateur ne pourrait pas modifier directement le mot de passe d’un utilisateur. En effet, l’administrateur ne doit pas connaître les mots de passe. Le site fournirait plutôt un bouton “Réinitialiser le mot de passe” à l’administrateur. Ce bouton générerait un mot de passe aléatoire qui serait envoyé par mail à l’utilisateur.
Aussi, dans une application plus avancée, on pourrait modifier certaines informations de l’utilisateur sans avoir besoin d’envoyer toutes les informations. Par exemple, le fait de passer un utilisateur administrateur se ferait plutôt par une action dédiée, sans toucher au reste du profil. Ou aussi, l’utilisateur ne devrait pas à avoir à modifier son mot de passe chaque fois qu’il souhaite éditer son profil.
Enregistrement avec une adresse email valide
Dans beaucoup de sites Web, il est important de savoir si un utilisateur est bien réel. Pour ce faire, on peut utiliser une vérification de son numéro de portable, de sa carte bancaire, ou de la validation d’un captcha. Nous allons ici nous baser sur la vérification de l’adresse email.
De plus, cela nous permet d’éviter des fautes de frappe dans l’email. Aussi, en ayant associé de manière sûre un email à un utilisateur, nous pourrions nous en servir pour une authentification à deux facteurs, ou pour renvoyer un mot de passe oublié…
Le nonce : un secret pour valider une adresse mail
Expliquons brièvement le mécanisme de validation par adresse email que nous
allons mettre en place. À la création d’un utilisateur, nous lui associons une
chaîne secrète de caractères aléatoires appelée nonce
cryptographique. Nous envoyons ce nonce
par email à l’adresse indiquée. La connaissance de ce nonce sert de preuve que
l’adresse email existe et que l’utilisateur y a accès. Il suffit alors à
l’utilisateur de renvoyer le nonce au site pour que ce dernier valide l’adresse
email (en mettant la valeur du nonce à la chaîne de caractère vide "" dans
notre cas).
Commençons par mettre à jour notre classe métier Utilisateur. Nous allons
rajouter des données nonce et email. Cependant, en cas de changement
d’adresse mail, nous souhaitons garder l’ancienne adresse mail en mémoire tant
que la nouvelle n’a pas été validée. Nous aurons donc une donnée emailAValider
en plus.
- Ajouter trois champs à la table
utilisateur:emailde typeVARCHAR(taille 256) nonNULL,emailAValiderde typeVARCHAR(taille 256) nonNULL,noncede typeVARCHAR(taille 32) nonNULL,
- Mettez à jour la classe métier
Utilisateur(dossiersrc/Modele/DataObject) :- ajoutez les attributs,
- mettez à jour le constructeur, les getters et les setters.
- Mettez à jour la classe de persistance
UtilisateurRepository:- mettez à jour
construireDepuisTableauSQL(qui permet de construire un utilisateur à partir de la sortie d’une requête SQL), - mettez à jour
getNomsColonnes, - mettez à jour la méthode
formatTableauSQL(qui fournit les données des requêtes SQL préparées).
- mettez à jour
Créons maintenant une classe utilitaire src/Lib/VerificationEmail.php. Cette
classe n’enverra pas encore de mail pour l’instant, mais affichera le contenu du
mail sur la page Web.
- Dans la classe
src/Configuration/ConfigurationSite.phpajoutez une fonction publique et statiquegetURLAbsoluequi renvoie la base de l’URL de votre site, par exemple :public static function getURLAbsolue() : string { return "http://localhost/tds-php/TD8/web/controleurFrontal.php"; } -
Créez la classe
src/Lib/VerificationEmail.phpavec le code suivant, que nous compléterons plus tard :namespace App\Covoiturage\Lib; use App\Covoiturage\Configuration\ConfigurationSite; use App\Covoiturage\Modele\DataObject\Utilisateur; class VerificationEmail { public static function envoiEmailValidation(Utilisateur $utilisateur): void { $destinataire = $utilisateur->getEmailAValider(); $sujet = "Validation de l'adresse email"; // Pour envoyer un email contenant du HTML $enTete = "MIME-Version: 1.0\r\n"; $enTete .= "Content-type:text/html;charset=UTF-8\r\n"; // Corps de l'email $loginURL = rawurlencode($utilisateur->getLogin()); $nonceURL = rawurlencode($utilisateur->getNonce()); $URLAbsolue = ConfigurationSite::getURLAbsolue(); $lienValidationEmail = "$URLAbsolue?action=validerEmail&controleur=utilisateur&login=$loginURL&nonce=$nonceURL"; $corpsEmailHTML = "<a href=\"$lienValidationEmail\">Validation</a>"; // Temporairement avant d'envoyer un vrai mail echo "Simulation d'envoi d'un mail<br> Destinataire : $destinataire<br> Sujet : $sujet<br> Corps : <br>$corpsEmailHTML"; // Quand vous aurez configué l'envoi de mail via PHP // mail($destinataire, $sujet, $corpsEmailHTML, $enTete); } public static function traiterEmailValidation($login, $nonce): bool { // À compléter return true; } public static function aValideEmail(Utilisateur $utilisateur) : bool { // À compléter return true; } } - Dans votre formulaire de création d’un utilisateur, rajoutez un champ pour
l’adresse email
<p class="InputAddOn"> <label class="InputAddOn-item" for="email_id">Email*</label> <input class="InputAddOn-field" type="email" value="" placeholder="toto@yopmail.com" name="email" id="email_id" required> </p> - Pour faire fonctionner l’action
creerDepuisFormulaire:- il faut que l’utilisateur créé avec
construireDepuisFormulairesoit correct :
Mettez à jour la méthodeconstruireDepuisFormulairepour qu’elle donne la valeur""à l’email, qu’elle stocke l’adresse mail du formulaire dansemailAValider, et qu’elle crée un nonce aléatoire à l’aide deMotDePasse::genererChaineAleatoire(). - il faut envoyer l’email de validation en cas de succès de la sauvegarde :
appelez la fonction
VerificationEmail::envoiEmailValidation.
- il faut que l’utilisateur créé avec
- Faisons en sorte que le lien envoyé par mail valide bien l’adresse mail :
- Codez la méthode
traiterEmailValidation()deVerificationEmail:
Si le login correspond à un utilisateur présent dans la base et que lenoncepassé en paramètres correspond aunoncede la BDD, alors coupez/collez l’email à valider dans l’email et passez à""le champnoncede la BDD. - Ajoutez une action
validerEmailau contrôleurUtilisateurqui récupère enGETdeux valeursloginetnonce(si elle existe sinon on appelleafficherErreur) et appelleVerificationEmail::traiterEmailValidation()avec ces valeurs. En cas de succès, on affiche la page de détail de cet utilisateur. En cas d’échec, on appelleafficherErreur.
- Codez la méthode
- Testez que la validation de l’email marche bien après la création d’un utilisateur en cliquant sur le lien de validation (qui, pour le moment, apparaît sur la page web après la création de l’utilisateur). Vérifiez dans la BDD que les données évoluent bien à chaque étape.
Nous allons maintenant pouvoir nous servir de la validation de l’email ailleurs dans le site.
- Modifiez l’action
connecterdu contrôleur utilisateur de sorte à accepter la connexion uniquement si l’utilisateur a validé un email.- Pour ceci, appelez la méthode
VerificationEmail::aValideEmail(). - Codez cette méthode pour qu’elle regarde si l’utilisateur a un email
différent de
"".
- Pour ceci, appelez la méthode
-
Dans l’action
creerDepuisFormulairedu contrôleur utilisateur, vérifiez que l’adresse email envoyée par l’utilisateur en est bien une. Pour cela, vous pouvez par exemple utiliser la fonctionfilter_varavec le filtreFILTER_VALIDATE_EMAIL. Cette fonction renverrafalsesi la donnée passée en paramètre ne valide pas le filtre spécifié. - Mise à jour d’un utilisateur :
- rajoutez un champ Email prérempli,
- dans l’action
mettreAJour, si l’email a changé, vérifiez le format de l’email puis écrivez-le dans le champemailAValider. Créez aussi un nonce aléatoire et envoyez le mail de validation.
Configuration pour l’envoi de mail
L’envoi d’email par PHP nécessite une configuration. Voici diverses options possibles.
Sur webinfo
Si vous déployez votre site sur le serveur web de l’IUT webinfo (cf. Cours
1), la
fonction mail() est déjà configurée.
Cela vous servira notamment dans le cadre du site web développé dans la SAE
(pour le parcours RACDV) ou pour le projet (pour le parcours DACS et IAMSI),
où le site devra être déployé sur webinfo, à terme.
Pour éviter que le serveur mail de l’IUT ne soit blacklisté des serveurs de
mail, vous n’avez l’autorisation d’envoyer des emails uniquement vers le domaine
yopmail.com, dont le fonctionnement est le suivant : un mail envoyé à
bob@yopmail.com est immédiatement lisible sur
https://yopmail.com/fr/?”bob”. Si le lien
précédent ne marche pas, allez sur la page https://yopmail.com/fr/ et saisir le
nom du mail jetable “bob” en haut à gauche.
Sous Docker
Dans le cadre du TD, nous allons mettre en place un client et un serveur SMTP en local, dans le conteneur docker qui fait tourner notre serveur web.
Nous allons utiliser 2 outils :
- MSMTP : un client de mail (SMTP) qui permet de demander à un serveur SMTP d’envoyer des mails en ligne de commande. PHP appellera cette ligne de commande.
- Mailpit : un serveur de mail (SMTP) simpliste et une interface Web pour voir les mails envoyés. Le serveur de mail n’enverra pas vraiment de mail au destinataire, mais permettra de les afficher via son interface web.
-
Ouvrez un terminal dans votre conteneur Docker : dans Docker Desktop, ouvrez votre conteneur, puis cliquez sur l’onglet
Exec. Dans le terminal qui apparaît, tapezbashpour retrouver votre shell habituel. Enfin, exécutez les commandes suivantes :# Mise à jour des paquets apt-get update # Installer msmtp en désactivant AppArmor echo "msmtp msmtp/apparmor boolean false" | debconf-set-selections DEBIAN_FRONTEND=noninteractive apt install -y msmtp # Configuration de msmtp echo "account default host host.docker.internal port 1025 from ton_adresse@example.com auth off" > /var/www/html/.msmtprc # Configuration de PHP echo "sendmail_path = \"/usr/bin/msmtp -C /var/www/html/.msmtprc -t\"" >> /usr/local/etc/php/conf.d/php.iniRemarques : 1025 est le port du serveur SMTP (cf. plus bas),
host.docker.internalest le nom d’hôte de votre machine depuis un conteneur Docker. -
Redémarrer votre conteneur serveur Web pour qu’Apache recharge le fichier de configuration de PHP.
-
Dans votre machine hôte (pas sous Docker), ouvrez un terminal et exécutez la commande suivante pour créer un nouveau conteneur Docker qui contiendra Mailpit.
docker run -d --name=mailpit -p 8025:8025 -p 1025:1025 axllent/mailpit -
Ouvrez votre navigateur à l’URL http://localhost:8025/ pour ouvrir l’interface Web de Mailpit.
Alternative sous Docker
Une solution professionnelle serait d’utiliser une bibliothèque PHP, comme le
composant Mailer du framework
Symfony, ou
PHPMailer.
La façon la plus simple de les installer est d’utiliser le gestionnaire de
bibliothèques PHP composer, que nous verrons au semestre 4 pour les Parcours RACDV.
À noter que PHPMailer propose aussi une façon de s’installer sans composer.
Autres sécurisations
Passage des formulaires en post
À l’heure actuelle, le mot de passe transite en clair dans l’URL. Vous
conviendrez facilement que ce n’est pas top. Nous allons donc passer nos
formulaires en méthode POST si le site est en production, ou en méthode GET si
le site est en développement.
Il faudrait donc maintenant récupérer les variables à l’aide de $_POST ou
$_GET. Cependant, nos liens internes, tels que ‘Détails’ ou ‘Mettre à jour’
fonctionnent en passant les variables dans l’URL comme un formulaire GET. Nous
avons donc besoin d’être capable de récupérer les variables automatiquement dans
$_POST ou le cas échéant dans $_GET.
-
Dans la classe
src/Configuration/ConfigurationSite.phpajoutez une fonction publique et statiquegetDebugqui renvoie un booléen (bool)trueoufalse(selon si le site est en mode debug ou non). Pour l’instant, renvoyeztrue(debug activé), par exemple. -
La variable globale
$_REQUESTest similaire à$_GETet$_POST, à ceci près qu’elle est la fusion de ces tableaux. En cas de conflit, les valeurs de$_POSTécrasent celles de$_GET.Remplacez tous les
$_GETpar des appels à$_REQUEST.Aide : Utiliser la fonction de remplacement globale avec
Ctrl+Shift+R(sur tous les fichiers du dossierTD8) pour vous aider. -
Modifiez les vues contenant des formulaires liés à la gestion des utilisateurs (
formulaireCreation.php,formulaireMiseAJour.php,formulaireConnexion.php) etpreference/formulairePreference.phppour faire en sorte que la méthodepostsoit utilisée siConfigurationSite::getDebug()renvoiefalseou en méthodegetsinon. De la même manière, mettez aussi à jour les formulaires liés à la gestion des trajets. -
Vérifiez que tout fonctionne toujours en utilisant un des formulaires du site pendant en changeant la valeur retournée par
ConfigurationSite::getDebug(). Vérifiez notamment que quandConfigurationSite::getDebug()renvoiefalse, la méthodePOSTest bien utilisée (pas de données du formulaire dans le query string…).
Sécurité avancée
Remarquez que les mots de passe envoyés en POST sont toujours visibles car envoyé
en clair. Vous pouvez par exemple les voir dans l’onglet réseau des outils de
développement (raccourci F12) dans la section paramètres sous Firefox (ou Form
data sous Chrome).
Le fait de hacher les mots de passe (ou les numéros de carte de crédit) dans la base de données évite qu’un accès en lecture à la base (suite à une faille de sécurité) ne permette à l’attaquant de récupérer toutes les données de tous les utilisateurs.
On pourrait aussi hacher le mot de passe côté client, et n’envoyer que le mot de passe haché au serveur. Dans le cas d’une attaque de l’homme du milieu (où quelqu’un écoute vos communications avec le serveur), l’attaquant n’obtiendra que le mot de passe haché et pas le mot de passe en clair. Mais cela ne l’empêchera pas de pouvoir s’authentifier puisque l’authentification repose sur le mot passe haché qu’il a récupéré.
La seule façon fiable de sécuriser une application web est le recours au
chiffrement de l’ensemble des communications entre le client (browser) et le
serveur, via l’utilisation du protocole TLS sur http, à savoir
https. Cependant, la mise en place de cette infrastructure était jusqu’à présent
compliqué. Même si
elle s’est simplifiée considérablement récemment,
cela dépasse le cadre de notre cours.
Notes techniques supplémentaires
Malgré nos protections, il est toujours possible pour un attaquant d’essayer des couples login / mot de passe en passant par notre interface de connexion. Un site professionnel devrait donc implémenter une limite au nombre d’échecs d’authentification consécutifs lié à chaque login. En cas de trop nombreux échecs, le site pourrait verrouiller le compte (déverrouillage avec l’adresse mail validée), ou rajouter une temporisation.
Listons d’autres protections de l’authentification des mots de passe indispensable dans un site professionnel :
- minimum de 8 caractères,
- interdire les mots de passe communs, attendus ou compromis,
- ne pas utiliser de question de rappel (nom de votre chien, …)
- utilisation d’un token dans le formulaire de connexion pour vérifier que l’utilisateur s’est bien connecté à l’aide de ce formulaire. Le but est d’éviter le phishing qui vous invite à vous connecter sur une autre interface dans le but de voler vos identifiants (attaque CSRF).
Source :
-
Mais vous pouvez changer le code HTML avec les outils de développement ↩