Créez le point d’entrée d’authentification
Pour l'instant, nous n'avons pas vraiment répondu à la demande d'Amélie. Elle souhaite en effet que nous restreignions les accès aux formulaires aux seuls utilisateurs autorisés. Néanmoins, nous avons avancé, puisque nous pouvons désormais représenter les utilisateurs qui viennent sur notre application.
Nous avons créé notre entité User
, et elle implémente UserInterface
, ce qui la rend compatible avec le système de sécurité de Symfony. Nous allons donc maintenant voir comment fonctionne l’authentification en elle-même. Prenons l’exemple d’un utilisateur qui remplit un formulaire de connexion et clique sur “Envoyer”. Une requête d’authentification est donc envoyée vers notre application.
Comme nous l’avons vu précédemment, l’authentification se passe en fait en deux temps :
D’abord nous récupérons l’identifiant de l’utilisateur dans sa requête d’authentification, et nous regardons dans nos listes d’utilisateurs si nous connaissons cet identifiant. Si oui, nous récupérons tout l’objet utilisateur correspondant. C’est ce que fait notre UserProvider.
Nous récupérons ensuite son mot de passe dans la requête, et nous le comparons avec celui qui est enregistré dans notre objet utilisateur. Si les deux correspondent, l’authentification est réussie. Ce sera le travail d’un objet appelé Authenticator.
Dans Symfony, ces deux étapes sont matérialisées par un firewall. Le firewall est donc l’objet qui appellera d’abord notre UserProvider, puis un Authenticator.
Reprenez votre fichier config/packages/security.yaml
, et regardez juste après la section providers
:
security:
# …
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
Nous avons donc déjà deux firewalls paramétrés : dev
et main
. Vous pouvez avoir autant de firewalls que vous le souhaitez. Chaque firewall s’applique uniquement aux URL correspondant à la clé pattern
de sa configuration. Cette clé a comme valeur une expression régulière. Si vous ne renseignez pas de pattern, le firewall s’applique à toute l’application.
Par exemple, dans notre fichier, le firewall dev
s’applique à toutes les URL qui commencent par /_profiler/
, /_wdt/
, /css/
, /images/
ou /js/
. Le firewall main
, lui, s’applique à toute l’application.
Mais du coup ça veut dire que main
s’applique aussi sur les routes concernées par dev
?
En fait non, car pour Symfony, “premier arrivé, premier servi”. En clair, Symfony applique le premier firewall qui correspond au chemin et n’applique pas les suivants. Si ce firewall contient une clé de configuration security
définie à false
(comme le firewall dev
), aucune vérification de sécurité ne sera effectuée sur les routes qui correspondent à ce pattern. Dans le cas contraire, il va falloir savoir quoi faire.
Et lazy
, ça veut dire quoi ?
Ça veut dire que même si la route demandée entre dans le périmètre de notre firewall, celui-ci ne forcera l'authentification que si nous demandons une vérification de droits quelque part. Sinon, le firewall ne fera rien.
La première chose qu’un firewall peut faire, c’est nous permettre d’authentifier les utilisateurs. Comme nous l’avons vu, la première étape pour ça est de les récupérer à l’aide d’un UserProvider. Ça tombe bien, la commande make:user
exécutée précédemment a rajouté le nom de notre UserProvider à la clé provider
du firewall main
. Nous n’avons donc plus besoin de faire quoi que ce soit.
Par contre, si nous voulons sécuriser un minimum notre application, il va nous falloir gérer les mots de passe de nos utilisateurs.
Sécurisez les mots de passe avec les hacheurs
Le moyen le plus simple de commencer à enregistrer les mots de passe de nos utilisateurs est de créer un formulaire d’enregistrement. Bien sûr, Amélie ne souhaite pas que n’importe qui puisse s’inscrire, donc une fois que les membres de son équipe auront créé leur compte, ce formulaire sera retiré ou protégé derrière l’administration. En attendant, il nous aidera à mieux comprendre le fonctionnement de l’authentification.
Pour des raisons de sécurité évidente, nous n’allons pas sauvegarder les mots de passe en base de données tels quels. Imaginez que notre base de données fuite sur internet, on aurait accès à tous les mots de passe de tous nos utilisateurs.
Symfony va donc rendre ces mots de passe illisibles grâce à un objet appelé hacheur, pour que nous puissions les enregistrer sans risque. Cette transformation sera à sens unique et nous ne prenons ainsi pas le risque que quelqu’un décrypte ces mots de passe.
Mais s'ils sont transformés et illisibles, comment je fais pour vérifier le mot de passe d’un utilisateur qui se connecte ?
Pas de panique : pour faire simple, redonner le même mot de passe au même hacheur permettra de les comparer efficacement. Mais dans tous les cas, une fois encore, Symfony s’en chargera pour nous.
En attendant, commençons par créer notre formulaire d’enregistrement.
Vous pouvez créer rapidement une ébauche de formulaire d'enregistrement grâce à la commande
make:registration-form
.La commande ajoute un attribut pour s'assurer de l'unicité de chaque utilisateur en base de données.
La commande crée aussi un FormType adapté qui prend en paramètre le mot de passe de l'utilisateur en clair.
Le controller généré fait ensuite appel à un objet pour hacher ce mot de passe.
Dans le fichier src/Form/RegistrationFormType.php
qui a été créé, vous pouvez supprimer la case à cocher agreeTerms
qui ne nous servira pas (Amélie créera elle-même les utilisateurs, ce formulaire ne sera pas public).
La gestion du mot de passe mérite par contre qu’on s’y attarde un instant. Comme vous le voyez, le champ plainPassword
qui contiendra le mot de passe avant hachage n’est pas mappé. Ainsi, nous pouvons récupérer son contenu dans le controller src/Controller/RegistrationController.php
grâce à la ligne $form->get('plainPassword')->getData()
et la transformer sans risquer d’écraser une donnée existante dans l’entité.
Cet appel à $form->get(‘plainPassword’)->getData()
est encapsulé dans deux autres appels : on appelle le hacheur UserPasswordHasherInterface
avec $userPasswordHasher->hashPassword()
en lui passant notre objet User et le mot de passe entré dans le formulaire, puis on passe le résultat de cette opération à $user->setPassword()
.
Mais pourquoi il faut passer l’objet User alors que je dois quand même faire un $user->setPassword()
ensuite ?
Pour répondre à cette question, ouvrons config/packages/security.yaml
et regardons les toutes premières lignes :
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
Comme vous pouvez le deviner en lisant ces lignes, c’est l’endroit où nous pouvons choisir de quelle façon nous allons hacher les mots de passe de chaque classe d'utilisateur que nous pourrions avoir.
Comment ça, on peut choisir comment hacher les mots de passe ?
Tout à fait. Nous utilisons des algorithmes de hachage pour hacher nos algorithmes, et plusieurs sont disponibles. Nous pouvons faire ce choix classe par classe. Ici par exemple, nous choisissons auto
, ce qui veut dire que nous laissons Symfony choisir le bon algorithme, pour toutes les classes qui implémentent PasswordAuthenticatedUserInterface
, comme notre classe User
. C’est donc pour ça que nous passons notre objet $user
à notre hacheur : pour qu’il sache de quelle classe il s’agit, et quel algorithme utiliser.
Mais du coup pourquoi auto
? On ne ferait pas mieux de choisir nous-mêmes l’algorithme qu’on veut ?
À moins d’avoir des besoins très spécifiques, pas forcément. Symfony choisit par défaut un algorithme appelé BCRYPT pour le moment, car il offre le meilleur rapport sécurisation versus temps de génération. Ce réglage peut changer dans le futur, mais vous n’aurez pas à vous en faire, car Symfony prend cette possibilité en compte aussi.
Nous pouvons donc enregistrer nos nouveaux utilisateurs et leurs mots de passe en base de données. Vous pouvez même en créer un de suite en vous rendant sur /register
. Nous allons donc pouvoir commencer à les authentifier.
Authentifiez vos utilisateurs via un formulaire
Pour pouvoir authentifier nos utilisateurs, nous allons avoir besoin de configurer un objet dont je vous parle depuis le début de cette partie : un Authenticator. Symfony embarque nativement un nombre assez important d’Authenticators parmi les plus communs : formulaire de login, lien de login, authentification JSON… Et bien sûr la possibilité d’en rajouter ou de créer les vôtres.
Vous l’avez certainement deviné, celui pour les formulaires d’authentification nous intéresse particulièrement. C’est un authenticator embarqué, ce qui veut dire que nous n’avons besoin que d’un peu de configuration pour l’activer. Il nous faudra quand même de quoi construire et afficher le formulaire lui-même. Donc au minimum un controller pour renvoyer une page, et un template pour représenter cette page.
La commande
make:security:form-login
vous permet de créer facilement une page de login et un logout.Elle met à jour la configuration dans
config/packages/security.yaml
.Vous devez tout de même compléter la configuration du logout avec le nom d'une route vers laquelle rediriger les utilisateurs déconnectés.
À vous de jouer
Contexte
Si vous vous rendez sur les pages /register
ou /login
, vous allez vous rendre compte que le design ne correspond pas vraiment à nos attentes… et si ça ne nous convient pas, ça ne conviendra pas non plus à Amélie. De plus, nous ne sommes toujours pas sûrs que tout fonctionne. Il est temps de s'en assurer.
Consignes
Commencez par ajouter les champs qui manquent au formulaire d'enregistrement pour le faire correspondre à notre entité (en particulier les rôles, le prénom et le nom de famille) et ajouter les éventuelles options qui vous paraissent pertinentes.
Les rôles doivent être une collection de
checkbox
, avec possibilité de sélectionner plusieurs rôles. Les choix possibles sontROLE_USER
etROLE_ADMIN
(si vous vous demandez à quoi servent ces rôles, nous le verrons bientôt).Ensuite, changez le chemin de la route de la page d'enregistrement pour que celui-ci soit désormais
/admin/user/new
.Puis vous pourrez travailler un peu le design de ces pages pour les intégrer dans notre charte.
Vous devriez pouvoir créer un utilisateur, et essayer de vous connecter avec. Pour cela, rendez-vous sur la page /login
et remplissez le formulaire. Vous devriez ensuite voir dans la debug toolbar une icône indiquant que vous êtes connecté.
En résumé
Les Firewalls gèrent l’authentification dans nos applications.
Ils agissent en deux temps : les UserProviders récupèrent l’utilisateur, et l’Authenticator vérifie son identité.
Les UserPasswordHashers permettent de transformer les mots de passe utilisateur en les rendant illisibles pour plus de sécurité.
Maintenant que nous savons connecter un utilisateur, voyons comment restreindre les accès !