1 / 15

🔐Authentification, ACL et Voters

🎯 Objectifs de cette session

Maîtriser la sécurité dans Symfony : de l'authentification simple aux permissions granulaires avec les Voters.

📋Programme détaillé

🟢 Niveau Débutant

  • Configuration Security
  • Authentification de base
  • Gestion des rôles

🟡 Niveau Intermédiaire

  • Voters personnalisés
  • Contrôle d'accès avancé
  • Expressions de sécurité

🔴 Niveau Avancé

  • ACL (Access Control Lists)
  • Permissions granulaires
  • Sécurité complexe

🏗️ Prérequis

Avoir une entité User fonctionnelle (vue dans la session précédente) avec les interfaces UserInterface et PasswordAuthenticatedUserInterface.

⚙️Configuration Security de base

1Fichier config/packages/security.yaml

security:
    # Configuration du hashage des mots de passe
    password_hashers:
        App\Entity\User:
            algorithm: auto

    # Fournisseurs d'utilisateurs
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    # Configuration des firewalls
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                default_target_path: /dashboard
            logout:
                path: app_logout
                target: /

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

🔍Explication des sections

💡 Bonne pratique

Utilise algorithm: auto pour que Symfony choisisse automatiquement le meilleur algorithme de hashage disponible.

📝Formulaires d'authentification

2Génération automatique

# Générer le système d'authentification complet
php bin/console make:auth

# Générer le formulaire d'inscription
php bin/console make:registration-form

🔐Contrôleur de connexion

// src/Controller/SecurityController.php

namespace App\Controller;

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

class SecurityController extends AbstractController
{
    #[Route(path: '/login', name: 'app_login')]
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        // Rediriger si déjà connecté
        if ($this->getUser()) {
            return $this->redirectToRoute('dashboard');
        }

        // Récupérer les erreurs de connexion
        $error = $authenticationUtils->getLastAuthenticationError();
        $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
        // La déconnexion est gérée par la clé logout dans security.yaml
        throw new \LogicException('Cette méthode peut rester vide');
    }
}

✅ Avantages de make:auth

  • Configuration security.yaml automatique
  • Contrôleur et templates générés
  • Gestion des erreurs incluse
  • Redirection après connexion

🎨Template de connexion

📄templates/security/login.html.twig

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

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

{% block body %}
{% if error %}
{{ error.messageKey|trans(error.messageData, 'security') }}
{% endif %} {% if app.user %}
Vous êtes connecté comme {{ app.user.userIdentifier }}, Se déconnecter
{% endif %}

Veuillez vous connecter

{% endblock %}

🔒 Sécurité du formulaire

  • CSRF Token - Protection contre les attaques CSRF
  • Remember Me - Cookie persistant optionnel
  • Autocomplete - Améliore l'UX tout en restant sécurisé

👥Système de rôles

🏗️Architecture des rôles Symfony

ROLE_USER - Rôle de base (automatique)
⬇️
ROLE_MODERATOR - Modération de contenu
⬇️
ROLE_ADMIN - Administration complète
⬇️
ROLE_SUPER_ADMIN - Super administrateur

⚡Hiérarchie des rôles

# config/packages/security.yaml
security:
    role_hierarchy:
        ROLE_MODERATOR: ROLE_USER
        ROLE_ADMIN: [ROLE_MODERATOR, ROLE_USER]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

✅ Avantages de la hiérarchie

  • Héritage automatique des permissions
  • Simplification du code
  • Maintenance facilitée

🎯 Usage dans les contrôleurs

// Un ROLE_ADMIN aura automatiquement
// ROLE_USER et ROLE_MODERATOR
$this->denyAccessUnlessGranted('ROLE_ADMIN');

🛡️Contrôle d'accès dans les contrôleurs

🔧Méthodes de protection

1. Annotations/Attributs

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

#[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController
{
    #[IsGranted('ROLE_USER')]
    #[Route('/profile')]
    public function profile(): Response
    {
        // Accessible aux utilisateurs connectés
    }
    
    #[IsGranted('ROLE_ADMIN')]
    #[Route('/admin/users')]
    public function adminUsers(): Response
    {
        // Accessible aux admins uniquement
    }
}

2. Méthodes dans le contrôleur

public function editArticle(Article $article): Response
{
    // Vérification simple
    $this->denyAccessUnlessGranted('ROLE_USER');
    
    // Vérification avec objet
    $this->denyAccessUnlessGranted('EDIT', $article);
    
    // Vérification conditionnelle
    if (!$this->isGranted('ROLE_ADMIN')) {
        throw $this->createAccessDeniedException();
    }
    
    // Récupérer l'utilisateur
    $user = $this->getUser();
}

🎯Vérifications dans les templates

{# templates/article/show.html.twig #}

{% if is_granted('ROLE_USER') %}
    Modifier
{% endif %}

{% if is_granted('EDIT', article) %}
    
{% endif %}

{% if app.user and app.user == article.author %}
    Vous êtes l'auteur de cet article
{% endif %}

⚠️ Sécurité importante

Ne jamais se fier uniquement aux vérifications côté template ! Toujours protéger les contrôleurs et les méthodes sensibles.

🗳️Introduction aux Voters

💡Qu'est-ce qu'un Voter ?

Un Voter est une classe qui décide si un utilisateur a le droit d'effectuer une action spécifique sur un objet donné.

🎯 Exemple concret

Question : "Est-ce que cet utilisateur peut modifier cet article ?"

Voter : Vérifie si l'utilisateur est l'auteur OU a le rôle ADMIN

🔄Fonctionnement d'un Voter

1. Contrôleur : $this->denyAccessUnlessGranted('EDIT', $article)
⬇️
2. Symfony : Cherche un Voter qui gère l'attribut 'EDIT'
⬇️
3. Voter : Analyse l'utilisateur et l'objet article
⬇️
4. Résultat : GRANTED (autorisé) ou DENIED (refusé)

✅ Avantages des Voters

  • Logique centralisée - Toute la logique d'autorisation au même endroit
  • Réutilisable - Un voter peut être utilisé partout
  • Testable - Facile de tester les règles d'autorisation
  • Flexible - Logique complexe possible

🛠️Création d'un Voter

3Commande de génération

# Générer un voter pour les articles
php bin/console make:voter ArticleVoter

📝Structure de base générée

// src/Security/Voter/ArticleVoter.php

namespace App\Security\Voter;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;

class ArticleVoter extends Voter
{
    public const EDIT = 'EDIT';
    public const VIEW = 'VIEW';
    public const DELETE = 'DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        // Remplace par ta propre logique
        return in_array($attribute, [self::EDIT, self::VIEW, self::DELETE])
            && $subject instanceof \App\Entity\Article;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        
        // Si l'utilisateur est anonyme, refuse l'accès
        if (!$user instanceof UserInterface) {
            return false;
        }

        // Tu connais maintenant $attribute, $subject, $user
        switch ($attribute) {
            case self::EDIT:
                return $this->canEdit($subject, $user);
            case self::VIEW:
                return $this->canView($subject, $user);
            case self::DELETE:
                return $this->canDelete($subject, $user);
        }

        return false;
    }
}

🔍 Méthodes clés

  • supports() - Détermine si ce voter gère cette requête
  • voteOnAttribute() - Logique principale d'autorisation

⚡Implémentation complète d'un Voter


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;
use Symfony\Component\Security\Core\User\UserInterface;

class ArticleVoter extends Voter
{
    public const EDIT = 'EDIT';
    public const VIEW = 'VIEW';
    public const DELETE = 'DELETE';
    public const PUBLISH = 'PUBLISH';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::VIEW, self::DELETE, self::PUBLISH])
            && $subject instanceof Article;
    }

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

        if (!$user instanceof UserInterface) {
            return false;
        }

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

        switch ($attribute) {
            case self::VIEW:
                return $this->canView($article, $user);
            case self::EDIT:
                return $this->canEdit($article, $user);
            case self::DELETE:
                return $this->canDelete($article, $user);
            case self::PUBLISH:
                return $this->canPublish($article, $user);
        }

        return false;
    }

    private function canView(Article $article, User $user): bool
    {
        // Un article publié peut être vu par tous les utilisateurs connectés
        if ($article->isPublished()) {
            return true;
        }

        // Un article non publié ne peut être vu que par son auteur ou un admin
        return $this->canEdit($article, $user);
    }

    private function canEdit(Article $article, User $user): bool
    {
        // L'auteur peut toujours modifier son article
        if ($article->getAuthor() === $user) {
            return true;
        }

        // Un admin peut modifier tous les articles
        return in_array('ROLE_ADMIN', $user->getRoles());
    }

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

    private function canPublish(Article $article, User $user): bool
    {
        // L'auteur peut publier son propre article
        if ($article->getAuthor() === $user) {
            return true;
        }

        // Un modérateur ou admin peut publier n'importe quel article
        return in_array('ROLE_MODERATOR', $user->getRoles()) || 
               in_array('ROLE_ADMIN', $user->getRoles());
    }
}

🎯 Logique d'autorisation implémentée

  • VIEW - Articles publics pour tous, privés pour auteur/admin
  • EDIT - Auteur ou admin
  • DELETE - Admin uniquement
  • PUBLISH - Auteur, modérateur ou admin

🎮Utilisation des Voters

🎯Dans les contrôleurs

// src/Controller/ArticleController.php

class ArticleController extends AbstractController
{
    #[Route('/article/{id}', name: 'article_show')]
    public function show(Article $article): Response
    {
        // Vérifier si l'utilisateur peut voir cet article
        $this->denyAccessUnlessGranted('VIEW', $article);
        
        return $this->render('article/show.html.twig', [
            'article' => $article,
        ]);
    }

    #[Route('/article/{id}/edit', name: 'article_edit')]
    public function edit(Article $article, Request $request): Response
    {
        // Vérifier si l'utilisateur peut modifier cet article
        $this->denyAccessUnlessGranted('EDIT', $article);
        
        // Logique d'édition...
        return $this->render('article/edit.html.twig', [
            'article' => $article,
        ]);
    }

    #[Route('/article/{id}/delete', name: 'article_delete', methods: ['POST'])]
    public function delete(Article $article): Response
    {
        // Vérifier si l'utilisateur peut supprimer cet article
        $this->denyAccessUnlessGranted('DELETE', $article);
        
        // Logique de suppression...
        return $this->redirectToRoute('article_list');
    }

    #[Route('/article/{id}/publish', name: 'article_publish', methods: ['POST'])]
    public function publish(Article $article): Response
    {
        // Vérifier si l'utilisateur peut publier cet article
        $this->denyAccessUnlessGranted('PUBLISH', $article);
        
        $article->setIsPublished(true);
        // Sauvegarder...
        
        return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
    }
}

🎨Dans les templates

{# templates/article/show.html.twig #}

{{ article.title }}

{{ article.content }}

{% if is_granted('EDIT', article) %}
Modifier
{% endif %}
{% if is_granted('DELETE', article) %}
{% endif %}
{% if is_granted('PUBLISH', article) and not article.isPublished %}
{% endif %}

💡 Avantage

Le même Voter est utilisé partout : contrôleurs, templates, services. La logique est centralisée et cohérente !

🚀Voters avancés

🎭Voter avec injection de dépendances


namespace App\Security\Voter;

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

class PremiumContentVoter extends Voter
{
    public const VIEW_PREMIUM = 'VIEW_PREMIUM';

    public function __construct(
        private SubscriptionService $subscriptionService
    ) {
    }

    protected function supports(string $attribute, mixed $subject): bool
    {
        return $attribute === self::VIEW_PREMIUM;
    }

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

        if (!$user instanceof User) {
            return false;
        }

        return match($attribute) {
            self::VIEW_PREMIUM => $this->canViewPremium($user),
            default => false,
        };
    }

    private function canViewPremium(User $user): bool
    {
        // Vérifier via un service externe
        return $this->subscriptionService->hasActivePremiumSubscription($user) ||
               in_array('ROLE_ADMIN', $user->getRoles());
    }
}

📊Voter pour les données relationnelles

private function canEditProject(Project $project, User $user): bool
{
    // L'utilisateur est-il membre de ce projet ?
    if ($project->getMembers()->contains($user)) {
        return true;
    }

    // L'utilisateur est-il propriétaire de l'organisation ?
    if ($project->getOrganization()->getOwner() === $user) {
        return true;
    }

    // L'utilisateur a-t-il un rôle admin dans l'organisation ?
    $membership = $project->getOrganization()
        ->getMemberships()
        ->filter(fn($m) => $m->getUser() === $user)
        ->first();

    return $membership && $membership->getRole() === 'admin';
}

🔧 Services injectables

  • EntityManager pour des requêtes complexes
  • Services métier personnalisés
  • Services externes (API, cache...)
  • Logger pour tracer les autorisations

🔐Introduction aux ACL

💡Qu'est-ce que les ACL ?

Access Control Lists - Système de permissions très granulaires au niveau de chaque objet en base de données.

🗳️ Voters vs ACL

Voters (Simple)

  • Logique dans le code
  • Règles fixes
  • Performance optimale
  • Maintenance facile

ACL (Avancé)

  • Permissions en base
  • Règles dynamiques
  • Très granulaire
  • Configuration complexe

🎯Cas d'usage des ACL

⚠️ Attention

Les ACL ajoutent de la complexité et des requêtes SQL supplémentaires. Utilise-les seulement si les Voters ne suffisent pas !

🎮 Exemple concret

"L'utilisateur Alice peut LIRE l'article #123, mais Bob peut LIRE et MODIFIER ce même article, et Carol peut tout faire dessus."

⚙️Configuration des ACL

4Installation du bundle ACL

# Installer le bundle ACL
composer require symfony/acl-bundle

# Créer les tables ACL
php bin/console acl:init

🗄️Tables créées automatiquement

🔧Configuration dans services.yaml

# config/services.yaml
services:
    # ACL Provider
    security.acl.provider:
        class: Symfony\Component\Security\Acl\Dbal\MutableAclProvider
        arguments:
            - '@doctrine.dbal.default_connection'
            - '@security.acl.permission_strategy'
            - '@security.acl.cache'

    # Permission Strategy
    security.acl.permission_strategy:
        class: Symfony\Component\Security\Acl\Domain\PermissionGrantingStrategy

    # ACL Cache
    security.acl.cache:
        class: Symfony\Component\Security\Acl\Domain\DoctrineAclCache
        arguments:
            - '@doctrine.orm.default_entity_manager'

🎭Permissions disponibles

Basiques

  • VIEW
  • EDIT
  • CREATE
  • DELETE

Avancées

  • MASTER
  • OWNER
  • OPERATOR

Personnalisées

  • PUBLISH
  • MODERATE
  • EXPORT

🛠️Utilisation des ACL

➕Création d'ACL pour un objet

// src/Controller/ArticleController.php

use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;

class ArticleController extends AbstractController
{
    public function createArticle(Request $request): Response
    {
        $article = new Article();
        // ... création de l'article ...

        // Créer l'ACL pour cet article
        $aclProvider = $this->container->get('security.acl.provider');
        $objectIdentity = ObjectIdentity::fromDomainObject($article);
        $acl = $aclProvider->createAcl($objectIdentity);

        // Donner tous les droits à l'auteur
        $securityIdentity = UserSecurityIdentity::fromAccount($this->getUser());
        $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
        
        // Donner le droit de lecture aux utilisateurs connectés
        $roleIdentity = new RoleSecurityIdentity('ROLE_USER');
        $acl->insertObjectAce($roleIdentity, MaskBuilder::MASK_VIEW);

        $aclProvider->updateAcl($acl);

        return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
    }
}

🔍Vérification des permissions

public function editArticle(Article $article): Response
{
    // Vérifier avec les ACL
    $this->denyAccessUnlessGranted('EDIT', $article);
    
    // Ou manuellement
    $authorizationChecker = $this->container->get('security.authorization_checker');
    if (!$authorizationChecker->isGranted('EDIT', $article)) {
        throw $this->createAccessDeniedException();
    }

    // ... logique d'édition ...
}

👥Gestion des permissions utilisateur

public function shareArticle(Article $article, User $user, string $permission): Response
{
    $this->denyAccessUnlessGranted('OWNER', $article);

    $aclProvider = $this->container->get('security.acl.provider');
    $objectIdentity = ObjectIdentity::fromDomainObject($article);
    $acl = $aclProvider->findAcl($objectIdentity);

    $securityIdentity = UserSecurityIdentity::fromAccount($user);
    
    switch ($permission) {
        case 'read':
            $mask = MaskBuilder::MASK_VIEW;
            break;
        case 'write':
            $mask = MaskBuilder::MASK_EDIT;
            break;
        case 'admin':
            $mask = MaskBuilder::MASK_OPERATOR;
            break;
    }

    $acl->insertObjectAce($securityIdentity, $mask);
    $aclProvider->updateAcl($acl);

    return $this->json(['status' => 'success']);
}

🎯 Masques de permissions

  • MASK_VIEW - Lecture seule
  • MASK_EDIT - Lecture + modification
  • MASK_OPERATOR - Tous droits sauf suppression
  • MASK_MASTER - Tous droits + gestion des ACL
  • MASK_OWNER - Propriétaire complet

🎯Bonnes pratiques et récapitulatif

✅Checklist de sécurité

🟢 Basique

  • ✓ Security.yaml configuré
  • ✓ Authentification fonctionnelle
  • ✓ Rôles bien définis
  • ✓ Protection des routes

🟡 Intermédiaire

  • ✓ Voters pour logique métier
  • ✓ Contrôles dans templates
  • ✓ Tests de sécurité
  • ✓ Gestion d'erreurs 403

🔴 Avancé

  • ✓ ACL si nécessaire
  • ✓ Audit des accès
  • ✓ Performance optimisée
  • ✓ Sécurité en profondeur

⚡Règles d'or

1. Sécurité par défaut - Tout est interdit sauf autorisation explicite
2. Défense en profondeur - Contrôles à tous les niveaux
3. Principe du moindre privilège - Donner le minimum nécessaire
4. Centralisation - Logique de sécurité dans les Voters

🎓Progression recommandée

  1. Commencer simple - Rôles et access_control
  2. Ajouter des Voters - Pour la logique métier
  3. Considérer les ACL - Seulement si vraiment nécessaire
  4. Tester intensivement - Sécurité = tests obligatoires

🎉 Félicitations !

Tu maîtrises maintenant l'authentification et l'autorisation dans Symfony ! Tu peux créer des applications sécurisées avec des permissions granulaires.

🚀 Prochaines étapes

  • Contrôleurs avancés - Services, injection de dépendances
  • Formulaires - Validation, types personnalisés
  • API REST - Sérialisation, API Platform
  • Tests - Tests unitaires et fonctionnels