Sécurité et utilisateurs avec Symfony 6

Symfony fournit de nombreux outils pour sécuriser votre application. Certains outils de sécurité liés à HTTP, comme les cookies de session sécurisés et la protection CSRF sont fournis par défaut. Le SecurityBundle, que vous découvrirez dans ce guide, fournit toutes les fonctionnalités d’authentification et d’autorisation nécessaires pour sécuriser votre application.

Pour commencer, installez le SecurityBundle :

composer require symfony/security-bundle

Si vous avez installé Symfony Flex, cela crée également un fichier de configuration security.yaml pour vous :

# config/packages/security.yaml
security:
    # https://symfony.com/doc/current/security.html#c-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        users_in_memory: { memory: null }
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: users_in_memory

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # 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 used
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }

C’est beaucoup de configuration ! Dans les sections suivantes, les trois éléments principaux sont abordés :

L’utilisateur
Toute section sécurisée de votre application a besoin d’un concept d’utilisateur. Le fournisseur d’utilisateur charge les utilisateurs à partir de n’importe quel stockage (par exemple, la base de données) sur la base d’un « identifiant utilisateur » (par exemple, l’adresse e-mail de l’utilisateur) ;

Le pare-feu et l’authentification des utilisateurs (firewalls)
Le pare-feu est au cœur de la sécurisation de votre application. Chaque demande au sein du pare-feu est vérifiée si elle nécessite un utilisateur authentifié. Le pare-feu se charge également d’authentifier cet utilisateur (par exemple, à l’aide d’un formulaire de connexion) ;

Contrôle d’accès (autorisation) (access_control)
En utilisant le contrôle d’accès et le vérificateur d’autorisation, vous contrôlez les permissions requises pour effectuer une action spécifique ou visiter une URL spécifique.

L’utilisateur

Les permissions dans Symfony sont toujours liées à un objet utilisateur. Si vous avez besoin de sécuriser (certaines parties de) votre application, vous devez créer une classe utilisateur. Il s’agit d’une classe qui implémente UserInterface. Il s’agit souvent d’une entité Doctrine, mais vous pouvez également utiliser une classe utilisateur Security dédiée.

La manière la plus simple de générer une classe utilisateur est d’utiliser la commande make:user du MakerBundle :

php bin/console make:user
 The name of the security user class (e.g. User) [User]:
 > User
 Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
 > yes
 Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
 > 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).
 Does this app need to hash/check user passwords? (yes/no) [yes]:
 > yes
 created: src/Entity/User.php
 created: src/Repository/UserRepository.php
 updated: src/Entity/User.php
 updated: config/packages/security.yaml

Si votre utilisateur est une entité Doctrine, comme dans l’exemple ci-dessus, n’oubliez pas de créer les tables en créant et en exécutant une migration :

// src/Entity/User.php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
 * @ORM\Entity(repositoryClass=UserRepository::class)
 */
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;
    /**
     * @ORM\Column(type="string", length=180, unique=true)
     */
    private $email;
    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];
    /**
     * @var string The hashed password
     * @ORM\Column(type="string")
     */
    private $password;
    public function getId(): ?int
    {
        return $this->id;
    }
    public function getEmail(): ?string
    {
        return $this->email;
    }
    public function setEmail(string $email): self
    {
        $this->email = $email;
        return $this;
    }
    /**
     * The public representation of the user (e.g. a username, an email address, etc.)
     *
     * @see UserInterface
     */
    public function getUserIdentifier(): string
    {
        return (string) $this->email;
    }
    /**
     * @see UserInterface
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';
        return array_unique($roles);
    }
    public function setRoles(array $roles): self
    {
        $this->roles = $roles;
        return $this;
    }
    /**
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): string
    {
        return $this->password;
    }
    public function setPassword(string $password): self
    {
        $this->password = $password;
        return $this;
    }
    /**
     * Returning a salt is only needed, if you are not using a modern
     * hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
     *
     * @see UserInterface
     */
    public function getSalt(): ?string
    {
        return null;
    }
    /**
     * @see UserInterface
     */
    public function eraseCredentials()
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }
}
php bin/console make:migration
php bin/console doctrine:migrations:migrate

Chargement de l’utilisateur : le fournisseur d’utilisateur

En plus de créer l’entité, la commande make:user ajoute également la configuration d’un fournisseur d’utilisateurs dans votre configuration de sécurité :

# config/packages/security.yaml
security:
    # ...
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email</pre>

Ce fournisseur d’utilisateurs sait comment (re)charger des utilisateurs à partir d’un stockage (par exemple une base de données) en se basant sur un « identifiant utilisateur » (par exemple l’adresse email ou le nom d’utilisateur de l’utilisateur). La configuration ci-dessus utilise Doctrine pour charger l’entité User en utilisant la propriété email comme « identifiant utilisateur ».

Les fournisseurs d’utilisateurs sont utilisés à plusieurs endroits au cours du cycle de vie de la sécurité :

Charger l’utilisateur en fonction d’un identifiant
Pendant le login (ou tout autre authentifiant), le fournisseur charge l’utilisateur sur la base de l’identifiant de l’utilisateur. Certaines autres fonctionnalités, comme l’usurpation d’identité et Remember Me, utilisent également cette fonction.
Recharger l’utilisateur à partir de la session
Au début de chaque requête, l’utilisateur est chargé à partir de la session (sauf si votre pare-feu est sans état). Le fournisseur « rafraîchit » l’utilisateur (c’est-à-dire que la base de données est à nouveau interrogée pour obtenir des données fraîches) pour s’assurer que toutes les informations relatives à l’utilisateur sont à jour (et si nécessaire, l’utilisateur est désauthentifié ou déconnecté si quelque chose a changé). Voir Sécurité pour plus d’informations sur ce processus.
Symfony est livré avec plusieurs fournisseurs d’utilisateurs intégrés :

Fournisseur d’utilisateur d’entité
Charge les utilisateurs depuis une base de données en utilisant Doctrine ;
Fournisseur d’utilisateur LDAP
Charge les utilisateurs à partir d’un serveur LDAP ;
Fournisseur d’utilisateur de mémoire
Charge les utilisateurs à partir d’un fichier de configuration ;
Fournisseur d’utilisateurs en chaîne
Fusionne deux ou plusieurs fournisseurs d’utilisateurs en un nouveau fournisseur d’utilisateurs.
Les fournisseurs d’utilisateurs intégrés couvrent les besoins les plus courants des applications, mais vous pouvez également créer votre propre fournisseur d’utilisateurs personnalisé.

Enregistrement de l’utilisateur : hachage des mots de passe

De nombreuses applications exigent qu’un utilisateur se connecte avec un mot de passe. Pour ces applications, le SecurityBundle fournit une fonctionnalité de hachage et de vérification des mots de passe.

Tout d’abord, assurez-vous que votre classe User implémente l’interface PasswordAuthenticatedUserInterface :

// src/Entity/User.php
// ...
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    // ...
    /**
     * @return string the hashed password for this user
     */
    public function getPassword(): string
    {
        return $this->password;
    }
}

Ensuite, configurez le hachage de mot de passe qui doit être utilisé pour cette classe. Si votre fichier security.yaml n’était pas déjà préconfiguré, alors make:user devrait l’avoir fait pour vous :

# config/packages/security.yaml
security:
    # ...
    password_hashers:
        # Use native password hasher, which auto-selects and migrates the best
        # possible hashing algorithm (which currently is "bcrypt")
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

Ensuite, configurez le hachage de mot de passe qui doit être utilisé pour cette classe.

// src/Controller/RegistrationController.php
namespace App\Controller;
// ...
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class RegistrationController extends AbstractController
{
    public function index(UserPasswordHasherInterface $passwordHasher)
    {
        // ... e.g. get the user data from a registration form
        $user = new User(...);
        $plaintextPassword = ...;
        // hash the password (based on the security.yaml config for the $user class)
        $hashedPassword = $passwordHasher->hashPassword(
            $user,
            $plaintextPassword
        );
        $user->setPassword($hashedPassword);
        // ...
    }
}

Note : La commande make:registration-form maker peut vous aider à configurer le contrôleur d’enregistrement et à ajouter des fonctionnalités comme la vérification de l’adresse e-mail en utilisant le SymfonyCastsVerifyEmailBundle.

composer require symfonycasts/verify-email-bundle
$ php bin/console make:registration-form

Vous pouvez également hacher manuellement un mot de passe en exécutant :

php bin/console security:hash-password

Pour en savoir plus sur tous les hachages disponibles et la migration des mots de passe, consultez la section Hachage et vérification des mots de passe.

Le pare-feu

La section « firewalls » de config/packages/security.yaml est la plus importante. Un « pare-feu » est votre système d’authentification : le pare-feu définit les parties de votre application qui sont sécurisées et la façon dont vos utilisateurs pourront s’authentifier (par exemple, formulaire de connexion, jeton API, etc).

# config/packages/security.yaml
security:
    # ...
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: users_in_memory
            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication
            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

Un seul pare-feu est actif pour chaque requête : Symfony utilise la clé de motif pour trouver la première correspondance (vous pouvez également faire correspondre par hôte ou d’autres choses).

Le pare-feu de développement est en fait un faux pare-feu : il s’assure que vous ne bloquez pas accidentellement les outils de développement de Symfony – qui se trouvent sous des URL comme /_profiler et /_wdt.

Toutes les URLs réelles sont gérées par le pare-feu principal (pas de clé de motif signifie qu’il correspond à toutes les URLs). Un pare-feu peut avoir plusieurs modes d’authentification, en d’autres termes, il permet de poser la question « Qui êtes-vous ? » de plusieurs façons.

Souvent, l’utilisateur est inconnu (c’est-à-dire qu’il n’est pas connecté) lorsqu’il visite votre site Web pour la première fois. Si vous visitez votre page d’accueil maintenant, vous aurez un accès et vous verrez que vous visitez une page derrière le pare-feu dans la barre d’outils :

La visite d’une URL sous un pare-feu ne nécessite pas nécessairement d’être authentifié (par exemple, le formulaire de connexion doit être accessible ou certaines parties de votre application sont publiques). Vous apprendrez comment restreindre l’accès aux URL, aux contrôleurs ou à toute autre chose au sein de votre pare-feu dans la section sur le contrôle d’accès.

Note : Si vous ne voyez pas la barre d’outils, installez le profileur avec :

composer require --dev symfony/profiler-pack

Authentification des utilisateurs

Lors de l’authentification, le système tente de trouver un utilisateur correspondant au visiteur de la page Web. Traditionnellement, cela se fait à l’aide d’un formulaire de connexion ou d’une boîte de dialogue HTTP de base dans le navigateur.

Note : Si votre application permet aux utilisateurs de se connecter via un service tiers tel que Google, Facebook ou Twitter (connexion sociale), consultez le bundle communautaire HWIOAuthBundle.

Formulaire de connexion

La plupart des sites Web ont un formulaire de connexion où les utilisateurs s’authentifient à l’aide d’un identifiant (par exemple, une adresse électronique ou un nom d’utilisateur) et d’un mot de passe. Cette fonctionnalité est fournie par le form login authenticator.

Tout d’abord, créez un contrôleur pour le formulaire de connexion :

php bin/console make:controller Login
 created: src/Controller/LoginController.php
 created: templates/login/index.html.twig
// src/Controller/LoginController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class LoginController extends AbstractController
{
    #[Route('/login', name: 'login')]
    public function index(): Response
    {
        return $this->render('login/index.html.twig', [
            'controller_name' => 'LoginController',
        ]);
    }
}

Ensuite, activez l’authentificateur de connexion par formulaire en utilisant le paramètre form_login :

# config/packages/security.yaml
security:
    # ...
    firewalls:
        main:
            # ...
            form_login:
                # "login" is the name of the route created previously
                login_path: login
                check_path: login

Le login_path et le check_path prennent en charge les URL et les noms de route (mais ne peuvent pas avoir de caractères génériques obligatoires – par exemple, /login/{foo} où foo n’a pas de valeur par défaut).

Une fois activé, le système de sécurité redirige les visiteurs non authentifiés vers le login_path lorsqu’ils essaient d’accéder à un endroit sécurisé (ce comportement peut être personnalisé en utilisant des points d’entrée d’authentification).

Modifiez le contrôleur de connexion pour rendre le formulaire de connexion :

Ne laissez pas ce contrôleur vous dérouter. Son rôle est seulement de rendre le formulaire : l’authentificateur form_login va gérer la soumission du formulaire automatiquement. Si l’utilisateur soumet un email ou un mot de passe invalide, l’authentificateur stockera l’erreur et redirigera vers ce contrôleur, où nous lirons l’erreur (en utilisant AuthenticationUtils) afin qu’elle puisse être affichée à l’utilisateur.

Enfin, créez ou mettez à jour le modèle :

{# templates/login/index.html.twig #}
{% extends 'base.html.twig' %}
{# ... #}
{% block body %}
    {% if error %}
        <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}
    <form action="{{ path('login') }}" method="post">
        <label for="username">Email:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}"/>
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password"/>
        {# If you want to control the URL the user is redirected to on success
        <input type="hidden" name="_target_path" value="/account"/> #}
        <button type="submit">login</button>
    </form>
{% endblock %}

Protection CSRF dans les formulaires de connexion

Les attaques CSRF de connexion peuvent être évitées en utilisant la même technique d’ajout de jetons CSRF cachés dans les formulaires de connexion. Le composant Sécurité fournit déjà une protection CSRF, mais vous devez configurer certaines options avant de l’utiliser.

Tout d’abord, vous devez activer CSRF sur le formulaire de connexion :

# config/packages/security.yaml
security:
    # ...
    firewalls:
        secured_area:
            # ...
            form_login:
                # ...
                enable_csrf: true

Ensuite, utilisez la fonction csrf_token() dans le modèle Twig pour générer un jeton CSRF et le stocker dans un champ caché du formulaire. Par défaut, le champ HTML doit être appelé _csrf_token et la chaîne utilisée pour générer la valeur doit être authenticate :

{# templates/security/login.html.twig #}
{# ... #}
<form action="{{ path('login') }}" method="post">
    {# ... the login fields #}
    <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
    <button type="submit">login</button>
</form>

Après cela, vous avez protégé votre formulaire de connexion contre les attaques CSRF.

Note : Vous pouvez modifier le nom du champ en définissant csrf_parameter et modifier l’ID du jeton en définissant csrf_token_id dans votre configuration. Voir la référence de la configuration de la sécurité (SecurityBundle) pour plus de détails.