🔐

Authentification Symfony 6.4

Guide complet pour débutants

🎯 Objectifs de cette présentation

  • Comprendre les concepts fondamentaux de l'authentification
  • Implémenter un système complet de connexion/inscription
  • Sécuriser votre application Symfony
  • Éviter les erreurs courantes

📚 Au programme

1
Introduction et concepts fondamentaux
2
Création de l'entité User
3
Configuration de la sécurité
4
Contrôleurs et formulaires
5
Templates et intégration
6
Erreurs courantes et bonnes pratiques

🎯 Pourquoi l'authentification ?

Votre application = Une maison sécurisée

🏠
Votre App
→
🔑
Authentification
→
👤
Utilisateurs

🎯 L'authentification permet de :

  • Identifier qui entre dans votre application
  • Contrôler l'accès aux différentes sections
  • Sécuriser les données sensibles
  • Personnaliser l'expérience utilisateur

⚠️ Sans authentification :

  • Accès libre à toutes les données
  • Pas de personnalisation possible
  • Aucune traçabilité des actions
  • Risques de sécurité majeurs

📚 Les 3 piliers de l'authentification Symfony

👤
User Entity

Carte d'identité

↔
📖
User Provider

Annuaire

↔
🛡️
Firewall

Garde de sécurité

1. L'entité User

class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    // Propriétés de l'utilisateur
}

→ Contient toutes les informations de l'utilisateur (email, mot de passe, rôles...)

2. Le User Provider

providers:
    app_user_provider:
        entity:
            class: App\Entity\User
            property: email

→ Dit à Symfony où et comment trouver les utilisateurs

3. Le Firewall

firewalls:
    main:
        provider: app_user_provider
        form_login:
            login_path: app_login

→ Définit les règles de sécurité et les méthodes d'authentification

🛠️ Étape 1 : Créer l'entité User

Commande magique :

php bin/console make:user

Questions interactives :

Question 1: The name of the security user class

→ Réponse: User


Question 2: Do you want to store user data in the database?

→ Réponse: yes


Question 3: Enter a property name for the unique "display" name

→ Réponse: email


Question 4: Will this app need to hash/check user passwords?

→ Réponse: yes

Code généré :

#[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, unique: true)]
    private ?string $email = null;

    #[ORM\Column]
    private array $roles = [];

    #[ORM\Column]
    private ?string $password = null;
}

🔧 Les méthodes clés de l'entité User

1. getUserIdentifier()

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

💡 Rôle : Retourne l'identifiant unique de l'utilisateur (ici l'email)

Symfony demande : "Comment identifier cet utilisateur ?" → On répond : "Par son email"

2. getRoles()

public function getRoles(): array
{
    $roles = $this->roles;
    // Garantir que tout utilisateur a au moins ROLE_USER
    $roles[] = 'ROLE_USER';
    
    return array_unique($roles);
}

💡 Rôle : Retourne les permissions de l'utilisateur

Tout utilisateur a automatiquement ROLE_USER

3. eraseCredentials()

public function eraseCredentials(): void
{
    // Effacer les données sensibles temporaires
    // $this->plainPassword = null;
}

💡 Rôle : Nettoie les données sensibles après l'authentification

Important pour ne pas garder le mot de passe en clair en mémoire

⚙️ Configuration security.yaml

1. Password Hashers

security:
    password_hashers:
        App\Entity\User:
            algorithm: auto  # Symfony choisit le meilleur algorithme

⚠️ JAMAIS stocker les mots de passe en clair !

Le hashage transforme "motdepasse123" en "$2y$13$xGxC..." (irréversible)

2. Providers

providers:
    app_user_provider:
        entity:
            class: App\Entity\User
            property: email  # Chercher par email

3. Firewall principal

firewalls:
    main:
        lazy: true
        provider: app_user_provider
        
        form_login:
            login_path: app_login
            check_path: app_login
            default_target_path: app_dashboard
            enable_csrf: true
            
        logout:
            path: app_logout
            target: app_home
            
        remember_me:
            secret: '%kernel.secret%'
            lifetime: 604800  # 1 semaine

🚦 Access Control - Qui va où ?

Configuration des accès

access_control:
    - { path: ^/login, roles: PUBLIC_ACCESS }
    - { path: ^/register, roles: PUBLIC_ACCESS }
    - { path: ^/admin, roles: ROLE_ADMIN }
    - { path: ^/profile, roles: ROLE_USER }
/login

✓ Tout le monde

/profile

🔐 Connecté

/admin

🛡️ Admin seul

Hiérarchie des rôles

role_hierarchy:
    ROLE_ADMIN: ROLE_USER
    ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

💡 Un ROLE_ADMIN a automatiquement ROLE_USER

Un ROLE_SUPER_ADMIN a tous les rôles inférieurs

🎮 Contrôleur de connexion

Génération automatique :

php bin/console make:auth

→ Choisir : 1 (Login form authenticator)

Méthode login()

#[Route('/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
    // Si déjà connecté, rediriger
    if ($this->getUser()) {
        return $this->redirectToRoute('app_dashboard');
    }

    // Récupérer l'erreur de connexion s'il y en a une
    $error = $authenticationUtils->getLastAuthenticationError();
    
    // Dernier nom d'utilisateur saisi
    $lastUsername = $authenticationUtils->getLastUsername();

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

Méthode logout()

#[Route('/logout', name: 'app_logout')]
public function logout(): void
{
    // Cette méthode peut rester vide
    // Symfony intercepte la route automatiquement
}

💡 Le firewall gère automatiquement la déconnexion !

Pas besoin de code, Symfony fait tout le travail

🎨 Template de connexion

Structure du formulaire

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

{% block body %}
    {# Affichage des erreurs #}
    {% if error %}
        
{{ error.messageKey|trans(error.messageData, 'security') }}
{% endif %} {# Message de bienvenue si déjà connecté #} {% if app.user %}
Vous êtes connecté en tant que {{ app.user.userIdentifier }}
{% endif %}
{# Token CSRF obligatoire #} {# Email #} {# Mot de passe #} {# Se souvenir de moi #}
{% endblock %}

⚠️ Points critiques :

  • _csrf_token : Protection contre les attaques CSRF
  • name="email" : Doit correspondre à votre configuration
  • _remember_me : Nom spécial reconnu par Symfony

📝 Système d'inscription

Génération du formulaire :

php bin/console make:registration-form

Contrôleur d'inscription

#[Route('/register', name: 'app_register')]
public function register(
    Request $request, 
    UserPasswordHasherInterface $userPasswordHasher, 
    EntityManagerInterface $entityManager
): Response {
    $user = new User();
    $form = $this->createForm(RegistrationFormType::class, $user);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // Encoder le mot de passe
        $user->setPassword(
            $userPasswordHasher->hashPassword(
                $user,
                $form->get('plainPassword')->getData()
            )
        );

        // Sauvegarder en base
        $entityManager->persist($user);
        $entityManager->flush();

        // Message de succès
        $this->addFlash('success', 'Compte créé avec succès !');

        return $this->redirectToRoute('app_login');
    }

    return $this->render('registration/register.html.twig', [
        'registrationForm' => $form->createView(),
    ]);
}
1. Formulaire

Données saisies

→
2. Validation

Vérification

→
3. Hash

Sécurisation

→
4. BDD

Sauvegarde

📋 FormType d'inscription

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('email', EmailType::class, [
                'label' => 'Adresse email',
                'attr' => ['class' => 'form-control']
            ])
            ->add('plainPassword', RepeatedType::class, [
                'type' => PasswordType::class,
                'mapped' => false,
                'first_options' => [
                    'label' => 'Mot de passe',
                    'attr' => ['class' => 'form-control']
                ],
                'second_options' => [
                    'label' => 'Confirmer le mot de passe',
                    'attr' => ['class' => 'form-control']
                ],
                'invalid_message' => 'Les mots de passe ne correspondent pas.',
                'constraints' => [
                    new NotBlank(['message' => 'Entrez un mot de passe']),
                    new Length([
                        'min' => 6,
                        'minMessage' => 'Le mot de passe doit contenir au moins {{ limit }} caractères',
                        'max' => 4096,
                    ]),
                ],
            ])
            ->add('agreeTerms', CheckboxType::class, [
                'mapped' => false,
                'label' => 'J\'accepte les conditions d\'utilisation',
                'constraints' => [
                    new IsTrue(['message' => 'Vous devez accepter nos conditions.']),
                ],
            ]);
    }
}

💡 Points importants :

  • RepeatedType : Deux champs identiques pour confirmation
  • mapped => false : Champ non lié à l'entité
  • constraints : Règles de validation

🔐 Protection des routes

Méthode 1 : Attribut IsGranted (Recommandé)

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

#[Route('/admin', name: 'app_admin')]
#[IsGranted('ROLE_ADMIN')]
public function admin(): Response
{
    return $this->render('admin/index.html.twig');
}

Méthode 2 : Dans le contrôleur

public function dashboard(): Response
{
    // Vérifier et lancer une exception si pas autorisé
    $this->denyAccessUnlessGranted('ROLE_USER');
    
    // Récupérer l'utilisateur connecté
    $user = $this->getUser();
    
    return $this->render('dashboard/index.html.twig', [
        'user' => $user
    ]);
}

Méthode 3 : Vérification manuelle

public function profile(): Response
{
    if (!$this->isGranted('ROLE_USER')) {
        throw $this->createAccessDeniedException('Accès refusé');
    }
    
    // Suite du code...
}

✅ Préférez l'attribut #[IsGranted()] : plus propre et lisible !

🎨 Authentification dans Twig

Navigation adaptative

{# Vérifier si l'utilisateur est connecté #}
{% if app.user %}
    
{% else %}
    
{% endif %}

Vérification des rôles

{# Afficher le menu admin uniquement aux admins #}
{% if is_granted('ROLE_ADMIN') %}
    
{% endif %}

{# Contenu différent selon le rôle #}
{% if is_granted('ROLE_PREMIUM') %}
    
{% else %}

Passez en Premium pour accéder à plus de fonctionnalités !

{% endif %}

Variables Twig utiles :

  • app.user : L'utilisateur connecté (null si pas connecté)
  • app.user.email : Email de l'utilisateur
  • app.user.roles : Tableau des rôles
  • is_granted('ROLE_X') : Vérifier un rôle

⚠️ Erreurs courantes et solutions

Erreur 1 : "User must implement UserInterface"

Cause : Votre classe User n'implémente pas les bonnes interfaces

// ❌ Incorrect
class User {
    // ...
}

// ✅ Correct
class User implements UserInterface, PasswordAuthenticatedUserInterface {
    // ...
}

Erreur 2 : Boucle de redirection infinie

Symptôme : "Trop de redirections"

Solution : Autoriser l'accès public aux pages de connexion

access_control:
    - { path: ^/login, roles: PUBLIC_ACCESS }  # IMPORTANT !
    - { path: ^/register, roles: PUBLIC_ACCESS }

Erreur 3 : Mot de passe non haché

Danger : Mot de passe stocké en clair !

// ❌ JAMAIS faire ça !
$user->setPassword($plainPassword);

// ✅ Toujours hasher
$hashedPassword = $userPasswordHasher->hashPassword($user, $plainPassword);
$user->setPassword($hashedPassword);

🗄️ Base de données et migrations

Étape 1 : Configurer la base

# Dans .env ou .env.local
DATABASE_URL="mysql://root:password@127.0.0.1:3306/symfony_auth"

Étape 2 : Créer la base

# Créer la base de données
php bin/console doctrine:database:create

Étape 3 : Créer la migration

# Générer le fichier de migration
php bin/console make:migration

Étape 4 : Appliquer la migration

# Exécuter la migration
php bin/console doctrine:migrations:migrate

💡 Commandes utiles :

# Voir le statut des migrations
php bin/console doctrine:migrations:status

# Voir le SQL qui sera exécuté
php bin/console doctrine:migrations:migrate --dry-run

# Annuler la dernière migration (attention !)
php bin/console doctrine:migrations:migrate prev

✅ Checklist de vérification

📋 Entité User

✓ Implémente UserInterface et PasswordAuthenticatedUserInterface
✓ getUserIdentifier() retourne l'email
✓ getRoles() retourne au moins ROLE_USER

⚙️ Configuration security.yaml

✓ password_hashers configuré avec algorithm: auto
✓ provider configuré avec la bonne classe et property
✓ firewall main avec form_login
✓ access_control autorise PUBLIC_ACCESS pour /login et /register

🎮 Contrôleurs et Templates

✓ SecurityController avec login() et logout()
✓ RegistrationController avec hashage du mot de passe
✓ Formulaires avec token CSRF
✓ Protection des routes avec #[IsGranted()]

🧪 Test de votre système

Scénario de test complet

1. Inscription

/register

→
2. Connexion

/login

→
3. Dashboard

/dashboard

→
4. Déconnexion

/logout

Points de vérification :

✅ Comportements attendus

  • Inscription crée un compte
  • Connexion redirige vers dashboard
  • Pages protégées inaccessibles sans connexion
  • Déconnexion redirige vers l'accueil
  • "Se souvenir de moi" fonctionne

❌ Problèmes à éviter

  • Mot de passe visible en base
  • Accès aux pages sans authentification
  • Erreurs CSRF
  • Boucles de redirection
  • Sessions qui expirent trop vite

🚀 Commande de test rapide :

# Démarrer le serveur de développement
php bin/console server:start

# Ouvrir dans le navigateur
# http://localhost:8000/register

🚀 Pour aller plus loin

Fonctionnalités avancées à implémenter

📧 Email et compte

  • Vérification d'email après inscription
  • Réinitialisation de mot de passe
  • Changement d'email avec confirmation
  • Notifications par email

🔐 Sécurité renforcée

  • Authentification à deux facteurs (2FA)
  • Limitation des tentatives de connexion
  • Journalisation des connexions
  • Sessions sécurisées

👥 Gestion utilisateurs

  • Profils utilisateur complets
  • Avatar et personnalisation
  • Système de rôles hiérarchiques
  • Tableau de bord admin

🌐 Intégrations

  • OAuth (Google, Facebook...)
  • API REST sécurisée
  • JWT pour les SPA
  • WebAuthn (biométrie)

📚 Ressources recommandées

🔌 Bundles utiles (pour plus tard)

ℹ️ Qu'est-ce qu'un Bundle ? C'est comme un plugin qui ajoute des fonctionnalités à Symfony. À installer une fois que vous maîtrisez les bases !

  • LexikJWTAuthenticationBundle

    🔐 Pour créer une API REST sécurisée. Utilise des tokens JWT au lieu des sessions. Niveau : Avancé

  • SchebTwoFactorBundle

    📱 Ajoute la double authentification (comme Google Authenticator). Renforce la sécurité. Niveau : Intermédiaire

  • HWIOAuthBundle

    🌐 Permet la connexion via Google, Facebook, GitHub... "Se connecter avec Google". Niveau : Intermédiaire

  • ResetPasswordBundle

    📧 Système complet "Mot de passe oublié ?" avec envoi d'email sécurisé. Niveau : Débutant-Intermédiaire

💡 Conseil : Commencez par maîtriser l'authentification de base avant d'ajouter ces bundles. Une fois à l'aise, le ResetPasswordBundle est un excellent premier ajout !

🎉

Félicitations !

Vous maîtrisez maintenant les bases de l'authentification Symfony !

✅ Ce que vous savez faire maintenant :

  • Créer un système complet d'authentification
  • Sécuriser vos routes et contrôleurs
  • Gérer les utilisateurs et leurs rôles
  • Implémenter connexion et inscription
  • Éviter les erreurs courantes de sécurité

N'oubliez pas les 3 règles d'or :

1. Hashage

Jamais de mots de passe en clair

2. CSRF

Toujours protéger vos formulaires

3. Rôles

Vérifier les permissions partout

Bon développement avec Symfony ! 🚀