🔐 Sécurité et Authentification

Protéger votre application Symfony

Users • Login • Firewall • Roles • Voters

Formation DWWM - 2025

Au programme 📋

# 🎯 Partie 1

Concepts de Sécurité

Authentification vs Autorisation

Authentification vs Autorisation 🔐

Analogie : Entrée dans un bâtiment sécurisé 🏢

Authentification = Prouver qui tu es

Autorisation = Vérifier ce que tu peux faire

🔑 Authentification

Question : Qui es-tu ?

Réponse : Login + mot de passe

Résultat : Tu es connecté

✅ Autorisation

Question : Que peux-tu faire ?

Réponse : Vérification des rôles

Résultat : Accès autorisé ou refusé

Composants de sécurité Symfony 🧩

Composant Rôle Fichier de config
User Entité utilisateur src/Entity/User.php
UserProvider Charge les utilisateurs depuis la BDD Automatique avec Doctrine
PasswordHasher Encode/vérifie les mots de passe security.yaml
Firewall Protège des URLs security.yaml
Access Control Règles d'accès par URL security.yaml
Authenticator Gère le processus de login src/Security/
Voters Logique d'autorisation personnalisée src/Security/Voter/

Workflow complet de sécurité 🔄

  1. Utilisateur tente d'accéder à /admin
    Requête HTTP vers une URL protégée
  2. Firewall intercepte
    "Cette URL est protégée, es-tu connecté ?"
  3. Vérification de session
    Session existante ? → Chargement du User
  4. Si non connecté → Redirection login
    Vers la page de connexion
  5. Soumission du formulaire de login
    Email + mot de passe
  6. Authenticator vérifie les credentials
    User existe ? Mot de passe correct ?
  7. Création de session
    User stocké en session
  8. Access Control vérifie les rôles
    Ce user a-t-il le rôle ROLE_ADMIN ?
  9. Autorisation accordée ou refusée
    Accès à /admin ou erreur 403
# 👤 Partie 2

Entité User

Créer la classe utilisateur

Créer l'entité User 🏗️

Commande interactive

php bin/console make:user

Exemple d'exécution

$ 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: config/packages/security.yaml

Code de l'entité User générée 💻

src/Entity/User.php

<?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)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 180)]
    private ?string $email = null;

    /**
     * @var list The user roles
     */
    #[ORM\Column]
    private array $roles = [];

    /**
     * @var string The hashed password
     */
    #[ORM\Column]
    private ?string $password = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): static
    {
        $this->email = $email;
        return $this;
    }

    /**
     * A visual identifier that represents this user.
     */
    public function getUserIdentifier(): string
    {
        return (string) $this->email;
    }

    /**
     * @return list
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    /**
     * @param list $roles
     */
    public function setRoles(array $roles): static
    {
        $this->roles = $roles;
        return $this;
    }

    /**
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): string
    {
        return $this->password;
    }

    public function setPassword(string $password): static
    {
        $this->password = $password;
        return $this;
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials(): void
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }
}

Comprendre les interfaces 📖

UserInterface

interface UserInterface
{
    public function getUserIdentifier(): string;  // Email ou username
    public function getRoles(): array;            // Rôles de l'user
    public function eraseCredentials(): void;     // Nettoyer données sensibles
}

Interface obligatoire pour tous les utilisateurs Symfony.

PasswordAuthenticatedUserInterface

interface PasswordAuthenticatedUserInterface
{
    public function getPassword(): ?string;  // Mot de passe hashé
}

Interface pour les utilisateurs avec mot de passe.

💡 getUserIdentifier() :

Retourne l'identifiant unique de l'utilisateur (email, username...). Utilisé par Symfony pour charger l'utilisateur.

Ajouter des champs à User 📝

Commande

php bin/console make:entity User

Exemple : Ajouter prénom et nom

$ php bin/console make:entity User

New property name (press <return> to stop adding fields):
> firstName

Field type (enter ? to see all types) [string]:
> string

Field length [255]:
> 100

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
> lastName

Field type [string]:
> string

Field length [255]:
> 100

Can this field be null in the database (nullable) (yes/no) [no]:
> no

Résultat dans User.php

#[ORM\Column(length: 100)]
private ?string $firstName = null;

#[ORM\Column(length: 100)]
private ?string $lastName = null;

// + getters et setters générés automatiquement

Créer la table en base de données 🗄️

Générer la migration

php bin/console make:migration

Exécuter la migration

php bin/console doctrine:migrations:migrate

✅ Résultat : Table user créée avec les colonnes :

# 🔒 Partie 3

Encodage des Mots de Passe

Hasher et vérifier les passwords

Pourquoi hasher les mots de passe ? 🤔

❌ JAMAIS stocker en clair

// ❌ DANGEREUX !
INSERT INTO user (email, password) VALUES ('user@example.com', 'motdepasse123');

// Si la BDD est piratée, tous les mots de passe sont exposés !

✅ TOUJOURS hasher

// ✅ SÉCURISÉ
INSERT INTO user (email, password) VALUES 
('user@example.com', '$2y$13$abcd1234...xyz7890');

// Hash irréversible, impossible de retrouver le mot de passe original

Analogie : La broyeuse à papier 🗑️

Un hash = passer un document dans une broyeuse :

On ne peut PAS retrouver le document original, mais on peut broyer un nouveau document et comparer les confettis !

Algorithme de hash Symfony 🔐

Symfony utilise par défaut l'algorithme bcrypt (ou argon2).

Configuration dans security.yaml

security:
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
        # 'auto' = Symfony choisit le meilleur algorithme (bcrypt ou argon2)

Exemple de hash bcrypt

Mot de passe : "monmotdepasse123"
Hash bcrypt  : "$2y$13$DfJ8k3.tL/rL5Pq0xYz.8eN7K2mP9qR4sT6uV8wX0yZ2aB4cD6eF8"

// Caractéristiques :
// - Commence par $2y$ (bcrypt)
// - $13$ = coût (plus c'est élevé, plus c'est sécurisé mais lent)
// - Reste = sel + hash

💡 Sel (salt) :

Chaîne aléatoire ajoutée au mot de passe avant le hash. Même mot de passe = hash différent à chaque fois !

Hasher un mot de passe 🔨

Dans un contrôleur (inscription)

use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

#[Route('/register', name: 'app_register')]
public function register(
    Request $request,
    UserPasswordHasherInterface $passwordHasher,
    EntityManagerInterface $em
): Response {
    $user = new User();
    $form = $this->createForm(RegistrationFormType::class, $user);
    
    $form->handleRequest($request);
    
    if ($form->isSubmitted() && $form->isValid()) {
        // Récupérer le mot de passe en clair depuis le formulaire
        $plainPassword = $form->get('plainPassword')->getData();
        
        // Hasher le mot de passe
        $hashedPassword = $passwordHasher->hashPassword(
            $user,
            $plainPassword
        );
        
        // Stocker le hash
        $user->setPassword($hashedPassword);
        
        $em->persist($user);
        $em->flush();
        
        return $this->redirectToRoute('app_login');
    }
    
    return $this->render('registration/register.html.twig', [
        'form' => $form
    ]);
}

Vérifier un mot de passe ✅

Comparer hash et mot de passe

use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

public function checkPassword(
    User $user,
    string $plainPassword,
    UserPasswordHasherInterface $passwordHasher
): bool {
    return $passwordHasher->isPasswordValid($user, $plainPassword);
}

// Utilisation
if ($passwordHasher->isPasswordValid($user, $submittedPassword)) {
    // Mot de passe correct !
} else {
    // Mot de passe incorrect
}

💡 Comment ça marche ?

  1. Récupère le hash stocké en BDD
  2. Hash le mot de passe soumis avec le même sel
  3. Compare les deux hash
  4. Retourne true/false
# 🚪 Partie 4

Authentification (Login/Logout)

Connexion et déconnexion

Créer le système de login 🏗️

Commande interactive

php bin/console make:auth

Exemple d'exécution

$ php bin/console make:auth

What style of authentication do you want? [Empty authenticator]:
  [0] Empty authenticator
  [1] Login form authenticator
> 1

The class name of the authenticator to create (e.g. AppCustomAuthenticator):
> LoginFormAuthenticator

Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
> SecurityController

Do you want to generate a '/logout' URL? (yes/no) [yes]:
> yes

 created: src/Security/LoginFormAuthenticator.php
 updated: config/packages/security.yaml
 created: src/Controller/SecurityController.php
 created: templates/security/login.html.twig

SecurityController généré 💻

src/Controller/SecurityController.php

<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
    #[Route(path: '/login', name: 'app_login')]
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        // Récupérer l'erreur de login s'il y en a une
        $error = $authenticationUtils->getLastAuthenticationError();
        
        // Dernier email saisi par l'utilisateur
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('security/login.html.twig', [
            'last_username' => $lastUsername,
            'error' => $error
        ]);
    }

    #[Route(path: '/logout', name: 'app_logout')]
    public function logout(): void
    {
        // Cette méthode peut rester vide
        // Symfony intercepte cette route et déconnecte l'utilisateur
        throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
    }
}

Template de login 🎨

templates/security/login.html.twig

{% extends 'base.html.twig' %}

{% block title %}Connexion{% endblock %}

{% block body %}
<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <h1 class="text-center mb-4">Connexion</h1>

            {% if error %}
                <div class="alert alert-danger">
                    {{ error.messageKey|trans(error.messageData, 'security') }}
                </div>
            {% endif %}

            <form method="post">
                <div class="mb-3">
                    <label for="username" class="form-label">Email</label>
                    <input type="email" 
                           class="form-control" 
                           id="username" 
                           name="_username" 
                           value="{{ last_username }}" 
                           required 
                           autofocus>
                </div>

                <div class="mb-3">
                    <label for="password" class="form-label">Mot de passe</label>
                    <input type="password" 
                           class="form-control" 
                           id="password" 
                           name="_password" 
                           required>
                </div>

                <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

                <div class="mb-3 form-check">
                    <input type="checkbox" class="form-check-input" id="remember_me" name="_remember_me">
                    <label class="form-check-label" for="remember_me">
                        Se souvenir de moi
                    </label>
                </div>

                <button type="submit" class="btn btn-primary w-100">
                    Se connecter
                </button>
            </form>
        </div>
    </div>
</div>
{% endblock %}

Configuration security.yaml 📝

config/packages/security.yaml

security:
    # Hashage des mots de passe
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

    # Provider : comment charger les utilisateurs
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    # Firewalls : zones de sécurité
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            lazy: true
            provider: app_user_provider
            
            # Authentification par formulaire
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            
            # Déconnexion
            logout:
                path: app_logout
                target: app_login

    # Contrôle d'accès
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }

Comment fonctionne le login ? 🔄

  1. Utilisateur visite /login
    Affichage du formulaire
  2. Soumission du formulaire vers /login
    Email + mot de passe envoyés
  3. Firewall intercepte (check_path)
    Symfony prend la main
  4. LoginFormAuthenticator s'exécute
    Récupère les credentials
  5. UserProvider charge l'utilisateur
    Cherche en BDD par email
  6. Vérification du mot de passe
    Compare le hash
  7. Si valide : Création de session
    User stocké en session
  8. Redirection vers la page d'origine
    Ou vers une page par défaut
# 🛡️ Partie 5

Firewall et Access Control

Protéger les URLs

C'est quoi un Firewall ? 🛡️

Analogie : Le checkpoint de sécurité ✋

Un firewall = un checkpoint qui vérifie chaque requête HTTP :

Configurer le Firewall 📝

Structure d'un firewall

firewalls:
    main:
        lazy: true                    # Chargement paresseux
        provider: app_user_provider   # Provider d'utilisateurs
        
        form_login:
            login_path: app_login     # Route du formulaire de login
            check_path: app_login     # Route de vérification (même route)
            enable_csrf: true         # Protection CSRF
            default_target_path: /    # Redirection après login
        
        logout:
            path: app_logout          # Route de déconnexion
            target: app_login         # Redirection après logout
        
        remember_me:
            secret: '%kernel.secret%' # Remember me (cookie)
            lifetime: 604800          # 1 semaine en secondes

Access Control - Règles d'accès 🚦

Access Control définit qui peut accéder à quelles URLs.

Configuration

access_control:
    # Tout le monde peut accéder
    - { path: ^/login, roles: PUBLIC_ACCESS }
    - { path: ^/register, roles: PUBLIC_ACCESS }
    
    # Seulement les utilisateurs connectés
    - { path: ^/profile, roles: ROLE_USER }
    
    # Seulement les admins
    - { path: ^/admin, roles: ROLE_ADMIN }
    
    # Seulement les super admins
    - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }

Ordre important !

⚠️ Attention : Les règles sont évaluées dans l'ordre. La première qui matche est appliquée !

# ❌ MAUVAIS ORDRE
- { path: ^/admin, roles: ROLE_USER }        # Matche /admin/users
- { path: ^/admin/users, roles: ROLE_ADMIN } # Jamais atteint !

# ✅ BON ORDRE
- { path: ^/admin/users, roles: ROLE_ADMIN } # Plus spécifique en premier
- { path: ^/admin, roles: ROLE_USER }        # Plus général ensuite
# 🎭 Partie 6

Rôles et Hiérarchie

Gérer les permissions

C'est quoi les rôles ? 🎭

Analogie : Les badges d'entreprise 🏢

Les rôles = différents types de badges :

Un utilisateur peut avoir plusieurs badges !

Rôle Description Accès typique
ROLE_USER Utilisateur basique /profile, /settings
ROLE_MODERATOR Modérateur /moderate, /reports
ROLE_ADMIN Administrateur /admin, /users, /stats
ROLE_SUPER_ADMIN Super administrateur Tout

Convention des noms de rôles 📋

💡 Règles de nommage :

Exemples valides

ROLE_USER
ROLE_ADMIN
ROLE_SUPER_ADMIN
ROLE_EDITOR
ROLE_MODERATOR
ROLE_CUSTOMER
ROLE_PREMIUM_USER

Exemples invalides

USER          // ❌ Manque ROLE_
role_admin    // ❌ Pas en majuscules
ROLE-ADMIN    // ❌ Tiret au lieu de underscore
ADMIN_ROLE    // ❌ ROLE_ doit être au début

Assigner des rôles à un utilisateur 👤

Dans un contrôleur

// Créer un admin
$user = new User();
$user->setEmail('admin@example.com');
$user->setRoles(['ROLE_ADMIN']);  // Tableau de rôles

$entityManager->persist($user);
$entityManager->flush();

// Ajouter un rôle à un utilisateur existant
$user = $userRepository->find(1);
$currentRoles = $user->getRoles();
$currentRoles[] = 'ROLE_MODERATOR';
$user->setRoles($currentRoles);

$entityManager->flush();

Stockage en BDD

// Colonne 'roles' de type JSON en BDD
{
    "id": 1,
    "email": "admin@example.com",
    "roles": ["ROLE_ADMIN", "ROLE_MODERATOR"]
}

💡 ROLE_USER automatique :

La méthode getRoles() de User ajoute automatiquement ROLE_USER à tous les utilisateurs.

Hiérarchie des rôles 📊

On peut définir qu'un rôle hérite automatiquement d'autres rôles.

Configuration dans security.yaml

security:
    role_hierarchy:
        ROLE_MODERATOR:    ROLE_USER
        ROLE_ADMIN:        ROLE_MODERATOR
        ROLE_SUPER_ADMIN:  ROLE_ADMIN

Résultat

ROLE_SUPER_ADMIN ↓ (hérite de) ROLE_ADMIN ↓ (hérite de) ROLE_MODERATOR ↓ (hérite de) ROLE_USER

✅ Conséquences :

# ✅ Partie 7

Vérifier les Permissions

isGranted et denyAccessUnlessGranted

Vérifier dans un contrôleur 🎮

Méthode 1 : isGranted()

public function admin(): Response
{
    // Vérifier si l'utilisateur a le rôle
    if (!$this->isGranted('ROLE_ADMIN')) {
        throw $this->createAccessDeniedException('Accès refusé !');
    }
    
    // Code accessible seulement aux admins
    return $this->render('admin/dashboard.html.twig');
}

Méthode 2 : denyAccessUnlessGranted()

public function admin(): Response
{
    // Lève automatiquement une exception si pas le rôle
    $this->denyAccessUnlessGranted('ROLE_ADMIN');
    
    // Code accessible seulement aux admins
    return $this->render('admin/dashboard.html.twig');
}

Méthode 3 : Attribut #[IsGranted]

use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/admin', name: 'admin_dashboard')]
#[IsGranted('ROLE_ADMIN')]
public function dashboard(): Response
{
    // Accès automatiquement protégé !
    return $this->render('admin/dashboard.html.twig');
}

Vérifier dans Twig 🎨

Afficher conditionnel

{% if is_granted('ROLE_ADMIN') %}
    <a href="{{ path('admin_dashboard') }}">
        Administration
    </a>
{% endif %}

{% if is_granted('ROLE_MODERATOR') %}
    <a href="{{ path('moderate_content') }}">
        Modération
    </a>
{% endif %}

Afficher le nom de l'utilisateur connecté

{% if app.user %}
    <p>Bonjour {{ app.user.email }} !</p>
    <a href="{{ path('app_logout') }}">Déconnexion</a>
{% else %}
    <a href="{{ path('app_login') }}">Connexion</a>
{% endif %}

Variable app.user

💡 Variable globale app.user :

Récupérer l'utilisateur connecté 👤

Dans un contrôleur

public function profile(): Response
{
    // Récupérer l'utilisateur connecté
    $user = $this->getUser();
    
    // Vérifier s'il est connecté
    if (!$user) {
        return $this->redirectToRoute('app_login');
    }
    
    // Utiliser l'objet User
    $email = $user->getEmail();
    $roles = $user->getRoles();
    
    return $this->render('profile/show.html.twig', [
        'user' => $user
    ]);
}

Type-hinting automatique

use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;

public function profile(Security $security): Response
{
    /** @var User|null $user */
    $user = $security->getUser();
    
    if (!$user) {
        return $this->redirectToRoute('app_login');
    }
    
    // $user est typé comme User
    return $this->render('profile/show.html.twig', [
        'user' => $user
    ]);
}
# ⚖️ Partie 8

Voters Personnalisés

Logique d'autorisation complexe

C'est quoi un Voter ? ⚖️

Analogie : Le jury de tribunal 👨‍⚖️

Un Voter = un juge qui décide si une action est autorisée :

Les Voters permettent une logique d'autorisation plus fine que les simples rôles !

Cas d'usage

Créer un Voter 🏗️

Commande

php bin/console make:voter ArticleVoter

src/Security/Voter/ArticleVoter.php

<?php
namespace App\Security\Voter;

use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ArticleVoter extends Voter
{
    const EDIT = 'ARTICLE_EDIT';
    const DELETE = 'ARTICLE_DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        // Ce voter gère seulement les attributs ARTICLE_EDIT et ARTICLE_DELETE
        if (!in_array($attribute, [self::EDIT, self::DELETE])) {
            return false;
        }

        // Ce voter gère seulement les objets Article
        if (!$subject instanceof Article) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        // L'utilisateur doit être connecté
        if (!$user instanceof User) {
            return false;
        }

        /** @var Article $article */
        $article = $subject;

        return match($attribute) {
            self::EDIT => $this->canEdit($article, $user),
            self::DELETE => $this->canDelete($article, $user),
            default => false
        };
    }

    private function canEdit(Article $article, User $user): bool
    {
        // Admins peuvent tout éditer
        if (in_array('ROLE_ADMIN', $user->getRoles())) {
            return true;
        }

        // Auteur peut éditer son propre article
        return $article->getAuthor() === $user;
    }

    private function canDelete(Article $article, User $user): bool
    {
        // Seuls les admins peuvent supprimer
        return in_array('ROLE_ADMIN', $user->getRoles());
    }
}

Utiliser un Voter 🎯

Dans un contrôleur

#[Route('/article/{id}/edit', name: 'article_edit')]
public function edit(Article $article): Response
{
    // Vérifier si l'utilisateur peut éditer cet article
    $this->denyAccessUnlessGranted('ARTICLE_EDIT', $article);
    
    // Si on arrive ici, l'utilisateur peut éditer
    // ...
}

#[Route('/article/{id}/delete', name: 'article_delete')]
public function delete(Article $article, EntityManagerInterface $em): Response
{
    // Vérifier si l'utilisateur peut supprimer
    if (!$this->isGranted('ARTICLE_DELETE', $article)) {
        throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer cet article');
    }
    
    $em->remove($article);
    $em->flush();
    
    return $this->redirectToRoute('article_list');
}

Dans Twig

{% if is_granted('ARTICLE_EDIT', article) %}
    <a href="{{ path('article_edit', {id: article.id}) }}" class="btn btn-primary">
        Modifier
    </a>
{% endif %}

{% if is_granted('ARTICLE_DELETE', article) %}
    <a href="{{ path('article_delete', {id: article.id}) }}" class="btn btn-danger">
        Supprimer
    </a>
{% endif %}
# 🍪 Partie 9

Remember Me

Se souvenir de moi

Activer Remember Me 🍪

Configuration dans security.yaml

firewalls:
    main:
        # ... autres configs
        
        remember_me:
            secret: '%kernel.secret%'     # Clé secrète
            lifetime: 604800               # 7 jours (en secondes)
            path: /                        # Portée du cookie
            always_remember_me: false      # Toujours se souvenir (sans checkbox)
            token_provider: 
                doctrine: true             # Stocker tokens en BDD (optionnel)

Ajouter la checkbox dans le formulaire de login

<div class="form-check">
    <input type="checkbox" 
           class="form-check-input" 
           id="remember_me" 
           name="_remember_me">
    <label class="form-check-label" for="remember_me">
        Se souvenir de moi
    </label>
</div>

💡 Comment ça marche ?

# 🔑 Partie 10

Réinitialisation de Mot de Passe

Mot de passe oublié

Processus de réinitialisation 🔄

  1. Utilisateur clique "Mot de passe oublié"
    Sur la page de login
  2. Formulaire : saisir email
    Page /reset-password
  3. Génération d'un token unique
    Token aléatoire + date d'expiration
  4. Envoi email avec lien
    /reset-password/reset/{token}
  5. Utilisateur clique sur le lien
    Vérification du token (valide ? pas expiré ?)
  6. Formulaire : nouveau mot de passe
    Saisir et confirmer
  7. Hash et sauvegarde
    Nouveau mot de passe hashé en BDD
  8. Suppression du token
    Token consommé, ne peut plus être réutilisé

Bundle SymfonyCasts Reset Password 📦

Installation

composer require symfonycasts/reset-password-bundle

Commande de configuration

php bin/console make:reset-password

✅ Fichiers créés :

💡 Ce bundle gère :

# 🎓 Récapitulatif

Ce qu'on a appris

Composants de sécurité - Résumé 📚

Composant Rôle Fichier
User Entité utilisateur (email, password, roles) src/Entity/User.php
PasswordHasher Hash et vérifie les mots de passe Service auto
Firewall Zone de sécurité (login/logout) security.yaml
Access Control Règles d'accès par URL security.yaml
Rôles Permissions (ROLE_USER, ROLE_ADMIN...) BDD + security.yaml
Voters Logique d'autorisation personnalisée src/Security/Voter/
Remember Me Cookie de persistance security.yaml

Workflow Login - Résumé 🔄

/login (GET) → Formulaire affiché
↓
/login (POST) → Email + Password soumis
↓
Firewall intercepte
↓
UserProvider charge User depuis BDD
↓
PasswordHasher vérifie le hash
↓
✅ Valide → Session créée → Redirection
❌ Invalide → Erreur → Retour formulaire

Commandes utiles 🛠️

Commande Description
make:user Créer l'entité User
make:auth Créer le système d'authentification
make:registration-form Créer le formulaire d'inscription
make:voter Créer un Voter personnalisé
make:reset-password Créer système de réinit mot de passe

Bonnes pratiques de sécurité 💡

  1. Toujours hasher les mots de passe
    JAMAIS en clair en BDD
  2. HTTPS en production
    Obligatoire pour protéger les credentials
  3. CSRF activé sur les formulaires
    Protection par défaut dans Symfony
  4. Hiérarchie des rôles
    Évite la duplication de permissions
  5. Voters pour logique complexe
    Plus maintenable que des if/else partout
  6. Access Control en premier
    Protection globale par URL
  7. Limite de tentatives de login
    Protéger contre brute force (bundle à installer)

Erreurs courantes à éviter ⚠️

❌ Erreur 1 : Mot de passe en clair

// ❌ DANGEREUX
$user->setPassword('motdepasse123');

// ✅ BON
$hashedPassword = $passwordHasher->hashPassword($user, 'motdepasse123');
$user->setPassword($hashedPassword);

❌ Erreur 2 : Oublier le préfixe ROLE_

// ❌ MAUVAIS
$user->setRoles(['ADMIN', 'USER']);

// ✅ BON
$user->setRoles(['ROLE_ADMIN', 'ROLE_USER']);

❌ Erreur 3 : Access Control mal ordonné

# ❌ MAUVAIS - Trop général en premier
- { path: ^/admin, roles: ROLE_USER }
- { path: ^/admin/users, roles: ROLE_ADMIN }  # Jamais atteint !

# ✅ BON - Plus spécifique en premier
- { path: ^/admin/users, roles: ROLE_ADMIN }
- { path: ^/admin, roles: ROLE_USER }

Ressources pour aller plus loin 📚

Documentation Symfony Security

🌐 https://symfony.com/doc/current/security.html

Guide complet de la sécurité

Authentication

🌐 https://symfony.com/doc/current/security/authentication.html

Tout sur l'authentification

Voters

🌐 https://symfony.com/doc/current/security/voters.html

Créer des autorisations personnalisées

Reset Password Bundle

🌐 https://github.com/SymfonyCasts/reset-password-bundle

Réinitialisation de mot de passe

Ce que vous maîtrisez maintenant ! 💪

Compétences acquises :

🎓 Félicitations !

Vous maîtrisez la Sécurité Symfony

Vous savez maintenant :

✅ Protéger votre application

✅ Gérer l'authentification

✅ Contrôler les autorisations

✅ Sécuriser les mots de passe

💡 Conseil final :

La sécurité n'est jamais "terminée". Restez à jour sur les bonnes pratiques, utilisez HTTPS en production, et n'oubliez jamais : JAMAIS de mots de passe en clair !

Vous maîtrisez Symfony ! 🚀

Formation DWWM - 2025