Deux notions majeures interviennent dans la conception de sécurité de Symfony :
Authentification : Qui êtes vous ? ; vous pouvez vous authentifier de plusieurs manières (HTTP authentification, certificat, formulaire de login, API, OAuth etc)
Authorization : Avez vous accès à ? ; permet d'autoriser de faire telle ou telle action ou accéder à telle page sans forcément savoir qui vous êtes, utilisateur anonyme par exemple.
Pour fonctionner, il est nécessaire d'ajouter le composant security à votre symfony.
composer require symfony/security-bundle
Si vous avez installé le projet avec la version complète (webapp), cette ligne n'est pas nécessaire.
La sécurité dans symfony implique plusieurs éléments :
Le firewall: qui est la porte d'entrée pour le système d'authentification, on définit différents firewall (au minimum 1 seul) qui va permettre de mettre en place le bon système de connexion pour l'url spécifiée via un pattern.
Le provider : qui permet au firewall d'interroger une collection d'utilisateurs/mot de passe ; C'est une sorte de base de tous les utilisateurs avec les mots de passe. Il existe deux type par défaut :
in memory : directement dans le fichier security.yml mais du coup les hash des mots de passes sont disponible dans un fichier
Entity : N'importe quelle entité qui implémente à minima les deux interfaces
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
Un encoder : qui permet de générer des hashs/d'encoder des mots de passe ; le plus connu étant MD5 mais vous pouvez utiliser d'autres encoders tels que : sha1, bcrypt ou plaintext (qui n'encode rien c'est le mot de passe en clair) http://symfony.com/doc/current/security/named_encoders.html
Les rôles : qui permettent de définir le niveau d'accès des utilisateurs connectés (authentifiés) et de configurer le firewall en fonction de ces rôles. Les rôles peuvent être hierarchisées afin d'expliquer par exemple qu'un administrateur (ROLE_ADMIN par exemple) et avant tout un utilisateur (ROLE_USER).
Le "guard" ou "authenticator" qui va gérer l'authentification, au travers de "passport". Il va par exemple vérifier que le couple login/mot de passe existe dans l'un des provider.
Configuration
A partir de la version 4, et avec le composant "maker", la gestion de la sécurité a été grandement facilitée. Là où sur les précédentes versions (la 2 notamment), il était d'usage de passer par un bundle tierce (FOSUserBundle par exemple), aujourd'hui cela n'est plus nécessaire.
Créer sa classe User
Si vous ne disposez pas encore d'une classe permettant la gestion des utilisateurs, il est possible d'en créer une avec la console. Si vous disposez déjà d'une classe utilisateur (ou que vous souhaitez utiliser plusieurs entités, il faudra modifier votre code en implémentant les interfaces UserInterface, PasswordAuthenticatedUserInterface et en implémentant les méthodes imposées par ces interfaces).
L'instruction ci-dessous permet de lancer la console pour créer la table User.
bin/console make:user
Symfony va vous poser plusieurs questions afin de configurer les éléments (le nom de l'entité, si vous utilisez doctrine, le champ correspondant au login, et l'encodage du password.
bin/console make:user davidannebicque@MacBook-Pro-de-David-2
Thenameofthesecurityuserclass (e.g. User) [User]:>Doyouwanttostoreuserdatainthedatabase (via Doctrine)? (yes/no) [yes]:>Enterapropertynamethatwillbetheunique"display"namefortheuser (e.g. email,username,uuid) [email]:> Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).
Doesthisappneedtohash/checkuserpasswords? (yes/no) [yes]:>created:src/Entity/User.phpcreated:src/Repository/UserRepository.phpupdated:src/Entity/User.phpupdated:config/packages/security.yamlSuccess!NextSteps:-ReviewyournewApp\Entity\Userclass.-Usemake:entitytoaddmorefieldstoyourUserentityandthenrunmake:migration.-Createawaytoauthenticate!Seehttps://symfony.com/doc/current/security.html
Une fois cette commande exécutée vous avez un fichier d'entité de créé, un repository associé, et le fichier security.yaml (dans config) qui a été mis à jour.
Le fichier entité
<?phpnamespaceApp\Entity;useApp\Repository\UserRepository;useDoctrine\ORM\Mappingas ORM;useSymfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;useSymfony\Component\Security\Core\User\UserInterface;#[ORM\Entity(repositoryClass:UserRepository::class)]classUserimplementsUserInterface,PasswordAuthenticatedUserInterface{ #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]private?int $id =null; #[ORM\Column(length:180, unique:true)]private?string $email =null; #[ORM\Column]privatearray $roles = [];/** * @varstring The hashed password */ #[ORM\Column]private?string $password =null;publicfunctiongetId():?int {return$this->id; }publicfunctiongetEmail():?string {return$this->email; }publicfunctionsetEmail(string $email):self {$this->email = $email;return$this; }/** * A visual identifier that represents this user. * * @see UserInterface */publicfunctiongetUserIdentifier():string {return (string) $this->email; }/** * @see UserInterface */publicfunctiongetRoles():array { $roles =$this->roles;// guarantee every user at least has ROLE_USER $roles[] ='ROLE_USER';returnarray_unique($roles); }publicfunctionsetRoles(array $roles):self {$this->roles = $roles;return$this; }/** * @see PasswordAuthenticatedUserInterface */publicfunctiongetPassword():string {return$this->password; }publicfunctionsetPassword(string $password):self {$this->password = $password;return$this; }/** * @see UserInterface */publicfunctioneraseCredentials() {// If you store any temporary, sensitive data on the user, clear it here// $this->plainPassword = null; }}
Le fichier security.yaml
Ce fichier fait le lien avec l'entité User (le provider), le login retenu (ici un email), et l'encodage du mot de passe, par défaut "auto"
security:# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwordspassword_hashers:Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:'auto'# https://symfony.com/doc/current/security.html#loading-the-user-the-user-providerproviders:# used to reload user from session & other features (e.g. switch_user)app_user_provider:entity:class:App\Entity\Userproperty:emailfirewalls:dev:pattern:^/(_(profiler|wdt)|css|images|js)/security:falsemain:lazy:trueprovider: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# Easy way to control access for large sections of your site# Note: Only the *first* access control that matches will be usedaccess_control:# - { path: ^/admin, roles: ROLE_ADMIN }# - { path: ^/profile, roles: ROLE_USER }when@test:security:password_hashers:# By default, password hashers are resource intensive and take time. This is# important to generate secure password hashes. In tests however, secure hashes# are not important, waste resources and increase test times. The following# reduces the work factor to the lowest possible values.Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:algorithm:autocost:4# Lowest possible value for bcrypttime_cost:3# Lowest possible value for argonmemory_cost:10# Lowest possible value for argon
Mise à jour de la BDD
Il faut ensuite mettre à jour votre base de données, avec les commandes suivantes:
Une nouvelle fois la console va nous permettre de dégrossir le travail et produire le contrôleur, le fichier de configuration et le formulaire de connexion.
Cette commande, comme indiqué génère plusieurs fichiers :
LoginAuthenticator.php : qui explique comment on authentifie un utilisateur (au travers de passeport)
SecurityController.php : Car ici, nous avons choisi une connexion avec formulaire, ce contrôleur permettra d'afficher la page de login, de récupérer les informations et de gérer la déconnexion
login.html.twig : qui contient le formulaire
Le fichier security.yaml est mis à jour pour faire le lien avec cet authenticator.
Attention !! Il faut modifier la ligne 51 avec une route qui existe dans votre projet
SecurityController
<?phpnamespaceApp\Controller;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\HttpFoundation\Response;useSymfony\Component\Routing\Annotation\Route;useSymfony\Component\Security\Http\Authentication\AuthenticationUtils;classSecurityControllerextendsAbstractController{ #[Route(path:'/login', name:'app_login')]publicfunctionlogin(AuthenticationUtils $authenticationUtils):Response {// if ($this->getUser()) {// return $this->redirectToRoute('target_path');// }// get the login error if there is one $error = $authenticationUtils->getLastAuthenticationError();// last username entered by the user $lastUsername = $authenticationUtils->getLastUsername();return$this->render('security/login.html.twig', ['last_username'=> $lastUsername,'error'=> $error]); } #[Route(path:'/logout', name:'app_logout')]publicfunctionlogout():void { throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}}
login.html.twig
{% extends'base.html.twig' %}{% block title %}Log in!{% endblock %}{% block body %}<formmethod="post"> {% if error %} <divclass="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> {% endif %} {% if app.user %} <divclass="mb-3"> You are logged in as {{ app.user.userIdentifier }}, <ahref="{{ path('app_logout') }}">Logout</a> </div> {% endif %} <h1class="h3 mb-3 font-weight-normal">Please sign in</h1> <labelfor="inputEmail">Email</label> <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" autocomplete="email" required autofocus>
<labelfor="inputPassword">Password</label> <input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" required>
<inputtype="hidden"name="_csrf_token"value="{{ csrf_token('authenticate') }}" >{# Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. See https://symfony.com/doc/current/security/remember_me.html <div class="checkbox mb-3"> <label> <input type="checkbox" name="_remember_me"> Remember me </label> </div> #} <buttonclass="btn btn-lg btn-primary"type="submit"> Sign in </button></form>{% endblock %}
Security.yaml mis à jour
security:# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwordspassword_hashers:Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:'auto'# https://symfony.com/doc/current/security.html#loading-the-user-the-user-providerproviders:# used to reload user from session & other features (e.g. switch_user)app_user_provider:entity:class:App\Entity\Userproperty:emailfirewalls:dev:pattern:^/(_(profiler|wdt)|css|images|js)/security:falsemain:lazy:trueprovider:app_user_providercustom_authenticator:App\Security\LoginAuthenticatorlogout:path:app_logout# where to redirect after logout# target: app_any_route# 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# Easy way to control access for large sections of your site# Note: Only the *first* access control that matches will be usedaccess_control:# - { path: ^/admin, roles: ROLE_ADMIN }# - { path: ^/profile, roles: ROLE_USER }when@test:security:password_hashers:# By default, password hashers are resource intensive and take time. This is# important to generate secure password hashes. In tests however, secure hashes# are not important, waste resources and increase test times. The following# reduces the work factor to the lowest possible values.Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:algorithm:autocost:4# Lowest possible value for bcrypttime_cost:3# Lowest possible value for argonmemory_cost:10# Lowest possible value for argon
La partie firewall est modifiée pour indiqué quel authenticator utiliser. On pourrait en avoir plusieurs.
Comme indiqué cette commande va créer plusieurs fichiers :
src/Security/LoginAuthenticator.php : qui va contenir la logique de votre authentification. Que faire une fois l'authetification réussie, ou en cas d'échec. Comment récupérer les informations de l'utilisateur.
src/Controller/SecurityController.php : qui va être le contrôleur gérant la partie sécurité et authentification. Par défaut la méthode login pour afficher le formulaire et traiter (avec l'aide de l'authenticator précédent), valider les données. C'est dans ce contraôleur que vous pouvez ajouter la déconnexion, l'enregistrement et le mot de passe perdu par exemple.
templates/security/login.html.twig : la vue contenant le formulaire de connexion, que vous pouvez librement adapter.
Mettre à jour le fichier security.yaml afin qu'il fasse le lien avec les différents éléments de sécurité.
Les étapes suivantes expliques ce qui a été créé.
Analyse du fichier security.yaml
Quasiment toute la sécurité se joue dans le fichier security.yaml qui fait le lien entre les différents éléments et gère les accès.
L'encodage du mot de passe (encoders) :
Tout est pré-confiuré, vous pouvez bien sûr adapter. L'encodage permet de définir le "format" de cryptage du mot de passe. Par défaut c'est "auto", c'est à dire que selon votre configuration Symfony choisira le niveau le plus élevé possible (bcrypt ou Argon2i)
Le Provider permet de faire le lien avec une source de données contenant les couples login/mot de passe ou des clés d'API... Les providers peuvent être des entités, des données "in_memory", ... Cette partie est configurée a été configurée suite à la création de l'entité User, avec la méthode de connection (login), ici l'email sera utilisé. Il est possible de coupler plusieurs provider tant que l'email est unique sur l'ensemble des sources.
providers:# used to reload user from session & other features (e.g. switch_user)app_user_provider:entity:class:App\Entity\Userproperty:email
L'authentification et le firewall
C'est la partie essentielle du process de sécurisation. C'est lui qui permet de dire quand il faut vérifier et authentifier un utilisateur. Le firewall permet de déterminer pour un pattern d'url (une requête (request)), la méthode d'authentification à utiliser (une page de connexion, une clé d'API, une dépendance à un fournisseur OAuth, ...).
L'exemple ci-dessus permet de définir que pour les routes particulières (les assets, le profiler), il n'y a pas de vérification. Pour toutes les autres routes (main), il faudra utiliser le provider contenant nos User et l'authenticator gérant le formulaire de Login. C'est ici que l'on pourrait proposer plusieurs méthodes de connexion en ajoutant les authenticator adaptés.
La gestion des roles se fait dans la partie "access_control" du fichier security. Il permet de définir pour chaque pattern d'URL quel rôle peut y accèder. C'est là que l'on sécurise nos différentes parties. Il est donc important de construire et structurer nos URL correctement pour être efficace sur le filtrage. Il faut évidemment veiller à ce que les pages de connexion ne soient pas derrière une page sécurisée...
De cette manière les URL seront automatiquement bloquées si l'utilisateur ne dispose pas du bon rôle. Il est aussi possible de tester ce rôle directement dans un contrôleur ou dans une vue selon les besoins. Voir la documentation pour plus d'éléments sur ce point
Récupérer l'utilisateur connecté
Enfin, il est souvent nécessaire de récupérer les informations sur l'utilisateur connecté. Pour cela, dans un contrô leur il est possible d'utiliser directement l'instruction :
$user =$this->getUser();
Gérer la déconnexion
Sur la même idée que pour la connexion, il est possible de gérer la déconnexion. Pour cela, dans le fichier security.yaml, il faut définir le path (la route) pour la méthode qui gére la déconnexion, et la cible (une route, optionnelle), une fois la déconnexion réussie.
firewalls:main:# ...logout:path:app_logout# where to redirect after logout# target: app_any_route
La méthode dans le contrôleur peut se résumer à :
#[Route(path:'/logout', name:'app_logout')]publicfunctionlogout():void { throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
Cette méthode qui ne retourne rien, permet la déconnexion, et la redirection se fait via le target définit dans security.yaml.
Générer un mot de passe avec le bon encodage
Grâce à la console, il est possible de générer un mot de passe selon l'encodage utilisé par Symfony.
bin/consolesecurity:hash-password
Création d'un utilisateur
Il est possible d'ajouter des utilisateurs directement dans la base de données (par exemple avec PhpMyAdmin ou en mode console Mysql) avec le mot de passe encodé correctement ou alors en créant un formulaire d'inscription.
Dans la base de données
On va ajouter PhpMyAdmin à Docker, pour cela dans votre fichier docker-composer.yaml et ajouter les lignes ci-dessous :
Dans la table User ajouter une entrée, avec un mot de passe crypté, et un rôle, qui doit être un tableau, exemple : ["ROLE_ADMIN"]
Attention ! On modifie le docker-compose.yaml MMI pas celui de Symfony !!!
Avec un formulaire d'inscription
La aussi le maker peut nous aider grandement...
bin/console make:registration
Répondez aux questions, il est nécessaire d'installer un complément si vous voulez vérifier le mail de vos utilisateurs : composer require symfonycasts/verify-email-bundle
Exercice
Mettre en place une classe User, et créer le formulaire de connexion en suivant la documentation (une commande dans le maker existe).
Ajouter une page qui ne sera accessible qu'a des utilisateurs Admin.
Vous pouvez ajouter une fonction pour récupérer le mot de passe perdu (une commande dans le maker existe...)