• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 30/09/2019

Sécurisez l'accès de votre site web

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

À présent, il est temps de découvrir comment la sécurité est gérée avec Symfony. C'est une étape un peu technique mais très importante et cela va nous permettre d'avoir un espace membre non seulement fonctionnel mais également sécurisé !

Dans ce chapitre :

  • nous commencerons par un peu de théorie sur la sécurité avec Symfony,

  • puis nous créerons un espace membre.

C'est parti !

L'authentification vs. l'autorisation

Le contrôle de la sécurité sous Symfony est très avancé mais également très simple. Pour cela Symfony distingue :

  • l'authentification,

  • l'autorisation.

Prenons quelques minutes pour définir ces mécanismes, c'est important pour le reste du chapitre.

L'authentification, c'est le procédé qui permet de déterminer qui est votre visiteur. Il y a deux cas possibles :

  • le visiteur est anonyme car il ne s'est pas identifié,

  • le visiteur est membre de votre site car s'est identifié.

Sous Symfony, c'est le firewall qui prend en charge l'authentification.

Régler les paramètres du firewall va vous permettre de sécuriser le site. En effet, vous pouvez restreindre l'accès à certaines parties du site uniquement aux visiteurs qui sont membres. Autrement dit, il faudra que le visiteur soit authentifié pour que le firewall l'autorise à passer.

L'autorisation intervient après l'authentification. Comme son nom l'indique, c'est la procédure qui va accorder les droits d'accès à un contenu.  Sous Symfony, c'est l'access control qui prend en charge l'autorisation.

Prenons l'exemple de différentes catégories de membres. Tous les visiteurs authentifiés ont le droit de poster des messages sur le forum mais uniquement les membres administrateurs ont des droits de modération et peuvent les supprimer. C'est l'access control qui permet de faire cela.

Etudions quelques cas de figure

Pour nous assurer que vous distinguez bien authentification et autorisation, nous allons voir ensemble quelques cas de figure tirés de la documentation officielle. Ils vont nous permettre d'identifier chaque composants. 

Cas n°1 : Un utilisateur anonyme souhaite se rendre sur la page /foo . Cette page est accessible sans identification.

Comme il n'est pas nécessaire d'avoir des droits spécifiques pour accéder à cette page, tous les visiteurs vont y accéder après avoir passé le pare-feu.

Le schéma suivant montre les différentes étapes du mécanisme de sécurité et distingue la partie "authentification" (le firewall) de la partie "autorisation ("access control") :

Mécanisme de sécurité pour une page qui ne requiert pas de droit.
Mécanisme de sécurité pour une page qui ne requiert pas de droit.
  1. Un visiteur anonyme souhaite afficher la page  /foo .

  2. La configuration du pare-feu a été réalisée pour que cette page soit accessible sans identification. Le visiteur peut donc passer.

  3. L'access control, ou contrôle d'accès, vérifie si la page  /foo  demande des droits particuliers. Ce n'est pas le cas donc le visiteur passe.

  4. La page  /foo  s'affiche pour le visiteur.

Cas n°2 : Un utilisateur anonyme souhaite se rendre sur la page  admin/foo. Cette page demande des droits d'accès particulier.

Cette fois, notre visiteur anonyme souhaite afficher la page  /admin/foo . Mais l'accès à cette page est limité aux utilisateurs qui on un rôle ROLE_ADMIN. L'accès lui est donc refusé :

Mécanisme de sécurité pour une page qui requiert des droits particuliers : visiteur anonyme.
Mécanisme de sécurité pour une page qui requiert des droits particuliers : visiteur anonyme.
  1. Le visiteur anonyme souhaite afficher la page  /admin/foo  . 

  2. La configuration du pare-feu a été réalisée pour que cette page soit accessible sans identification. Le visiteur peut donc passer.

  3. Le contrôle d'accès vérifie si la page /admin/foo  demande des droits particuliers. C'est le cas : il est nécessaire d'avoir un rôle ROLE_ADMIN. Le contrôle d'accès interdit donc l'affichage de la page  /admin/foo  à notre utilisateur.

  4. Le visiteur est redirigé vers une page pour s'identifier.

Cas n°3 : Un utilisateur identifié souhaite se rendre sur la page  admin/foo . Cette page demande des droits d'accès particulier.

Notre visiteur n'est plus anonyme car il s'est identifié :

Mécanisme de sécurité pour une page qui requiert des droits particuliers : visiteur identifié.
Mécanisme de sécurité pour une page qui requiert des droits particuliers : visiteur identifié.
  1. Après l'étape d'identification, Brian souhaite afficher la page  /admin/foo . L'authentification est vérifiée par le firewall qui le laisse ensuite passer.

  2. Ensuite, le contrôle d'accès vérifie les droits pour la page  /admin/foo  . Le rôle ROLE_ADMIN est requis mais Brian n'en dispose pas. L'accès à  /admin/foo  lui est donc interdit.

  3. Cette fois, Brian est redirigé vers une page d'erreur par le contrôle d'accès, lui expliquant qu'il ne dispose pas des droits requis.

Cas n°4 : Un utilisateur identifié souhaite se rendre sur la page  admin/foo . L'utilisateur dispose des droits d'accès requis.

Le visiteur, Mickaël, s'est identifié et a des droits d'administrateurs. Puisqu'il a le rôle ROLE_ADMIN, il peut donc voir la page  /admin/foo  :

Mécanisme de sécurité pour une page qui requiert des droits particuliers : le visiteur a le rôle requis.
Mécanisme de sécurité pour une page qui requiert des droits particuliers : le visiteur a le rôle requis.
  1. Après son identification, Mickaël souhaite afficher /admin/foo .  L'authentification est vérifiée par le firewall qui le laisse ensuite passer.

  2. Le contrôle d'accès vérifie les droits pour la page  /admin/foo . Le rôle ROLE_ADMIN est requis et Mickaël en dispose bien. L'accès à  /admin/foo  est donc autorisé.

  3. La page /admin/foo s'affiche puis le visiteur a été authentifié et qu'il a les droits requis.

Passons à la pratique

Même si le mécanisme de sécurité est plutôt simple à appréhender, cela prend quand même un peu temps à mettre en place. Procédons par étape !

Configurez la sécurité

L'importance de la sécurité est telle qu'il y a un fichier de configuration dédié ! C'est le fichier security.yaml qui se trouve dans le répertoire config/packages de votre application. Pour l'instant, il n'y a pas grand chose dedans. Nous allons déjà y ajouter ces sections (elles sont décrites ensuite) :

# config/packages/security.yaml

security:
  encoders:
    Symfony\Component\Security\Core\User\User: plaintext

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

  providers:
    in_memory:
      memory:
        users:
          user:  { password: userpass, roles: [ 'ROLE_USER' ] }
          admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

  firewalls:
    dev:
      pattern: ^/(_(profiler|wdt)|css|images|js)/
      security: false

  access_control:
    #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }

Commençons par décortiquer chacune des sections de ce fichier de configuration.

Encoders

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

Comme son nom l'indique, l'objet encodeur va encoder les mots de passe des utilisateurs. C'est donc grâce à cette section que vous pourrez changer d'encodeur, c'est-à-dire la manière dont les mots de passe sont encodés dans votre application.

Ici, nous utilisons  plaintext qui laisse les mots de passe apparents (donc il n'encode rien en fait !). Nous choisirons un encodeur plus sûr plus tard.

role_hierarchy

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

Comme nous l'avons vu, le "rôle" de l'utilisateur est central pour l'autorisation. Il est possible de définir différents rôles et d'en attribuer un ou plusieurs pour un utilisateur donné. De même, l'accès à chaque ressource est conditionné par un ou plusieurs rôles. Le contrôle d'accès s'assure que l'utilisateur a le rôle nécessaire puis autorise (ou non) l'accès.

La hiérarchie des différents rôles est établie dans cette section. Par exemple, le rôle ROLE_USER est inclus dans le rôle ROLE_ADMIN. Concrètement, cela veut dire qu'un utilisateur qui a le rôle ROLE_ADMIN peut accéder à des pages qui nécessite seulement le rôle ROLE_USER.

Vous pouvez nommer vos rôles comme vous le souhaitez, l'important est qu'ils commencent par «  ROLE_  ».

Providers

security:
    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

Pour identifier les utilisateurs, le firewall demande à un provider. C'est un fournisseur d'utilisateurs. 

Si vous regardez le fichier, pour l'instant il n'y qu'un fournisseur et c'est  in_memory (vous pouvez tout à fait choisir un autre nom). C'est un fournisseur atypique utilisé pour le développement. En effet, les utilisateurs, ici user et admin, sont répertoriés dans le fichier de configuration. Ainsi, nous n'avons pas besoin d'une base de données pour tester la sécurité de notre application.

Firewalls

security:
    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

 Le firewall permet de vérifier l'identité de l'utilisateur. Dans notre cas, notre firewall est  dev  (ceux de démonstration ont été retirés). Il désactive la sécurité pour certaines URL. Nous y reviendrons.

access_control

security:
    access_control:
        #- { path: ^/login, roles: ROLE_ADMIN }

Le contrôle d'accès vérifie que l'utilisateur a le(s) rôle(s) requis pour accéder au contenu demandé. Les contrôles d'accès peuvent être utilisés :

  • à partir du fichier de configuration, comme c'est le cas ici. Pour cela, il faut appliquer règles sur des URL. Par exemple, on peut sécuriser toutes les URL commençant par /admin ;

  • dans les contrôleurs directement. Pour cela, il faut appliquer des règles sur les méthodes des contrôleurs. L'avantage est la grande liberté que cela apporte : les règles peuvent changer en fonction des paramètres.

Ces deux manières de faire sont complémentaires !

Créez un formulaire de connexion

Après cette présentation du fichier de configuration, vous devez mieux comprendre ce qui peut être configuré. Passons à l'étape suivante : l'authentification !

Pour cela, nous allons mettre en place un formulaire qui demande à l'utilisateur son adresse mail et un mot de passe.

Créez un formulaire

C'est le plus simple, il s'agit d'intégrer un simple formulaire HTML : faisons-le en Twig, par exemple.

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

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

    <div class="row">
        <div class="col-sm-5">
            <div class="well">
                <form action="{{ path('security_login') }}" method="post">
                    <fieldset>
                        <legend><i class="fa fa-lock" aria-hidden="true"></i> {{ 'title.login'|trans }}</legend>
                        <div class="form-group">
                            <label for="username">{{ 'label.username'|trans }}</label>
                            <input type="text" id="username" name="username" value="{{ last_username }}" class="form-control"/>
                        </div>
                        <div class="form-group">
                            <label for="password">{{ 'label.password'|trans }}</label>
                            <input type="password" id="password" name="password" class="form-control" />
                        </div>
                        <input type="hidden" name="_target_path" value="{{ app.request.get('redirect_to') }}"/>
                        <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/>
                        <button type="submit" class="btn btn-primary">
                            <i class="fa fa-sign-in" aria-hidden="true"></i> {{ 'action.sign_in'|trans }}
                        </button>
                    </fieldset>
                </form>
            </div>
        </div>
    </div>
{% endblock %}

Pour le rendu visuel correspondant dans le projet de démonstration de ce cours :

Capture d'écran du formulaire de connexion
Formulaire de connexion

Quelques contraintes sont à connaître :

  • le "login" doit avoir l'attribut  name  "username" ;

  • le "mot de passe" doit avoir l'attribut  name  "password".

Créez un contrôleur

Pour manipuler un formulaire de connexion, il nous faut un contrôleur ! Ce contrôleur aura deux fonctions : une pour la connexion et une pour la déconnexion.

<?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("/login", name="security_login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        // retrouver une erreur d'authentification s'il y en a une
        $error = $authenticationUtils->getLastAuthenticationError();
        // retrouver le dernier identifiant de connexion utilisé
        $lastUsername = $authenticationUtils->getLastUsername();

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

    /**
     * @Route("/logout", name="security_logout")
     */
    public function logout(): void
    {
        throw new \Exception('This should never be reached!');
    }
}

Quelques informations s'imposent. Tout d'abord, nous allons utiliser un service AuthenticationUtils dans la fonction de connexion, dont la responsabilité est de valider l'authentification.

La fonction de déconnexion doit vous surprendre ! Nous avons dit qu'une action doit toujours retourner une réponse : en fait, cette fonction ne sera jamais exécutée. Le composant Sécurité a seulement besoin d'une URL pour effectuer la déconnexion.

Il manque quelque chose pour faire le lien : la méthode d'authentification !

Créez de la méthode d'authentification

Pour créer une méthode d'authentification, nous allons utiliser une extension du composant Security appelée Guard. Changement majeur par rapport aux anciennes versions de Symfony, il est maintenant très facile de créer sa méthode d'authentification ou encore "authenticator".

Pour cela, nous allons créer une classe qui étend  AbstractFormLoginAuthenticator  du composant Guard. Il faudra implémenter et compléter quelques fonctions :

  • supports() : la fonction définit dans quelles conditions la classe sera appelée.

  • getCredentials() : retourne les éléments d'information d'authentification.

  • getUser() : retourne un utilisateur au sens Symfony (instance de UserInterface du composant Security).

  • checkCredentials() : contrôle à la connexion que les informations d'authentification sont valides.

  • onAuthenticationSuccess() : décide que faire dans le cas où l'utilisateur est bien authentifié, généralement une redirection vers une URL donnée.

  • getLoginUrl() : définit l’URL du formulaire de connexion, dans notre cas  security_login .

Voici une implémentation valide :

<?php

namespace App\Security;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
    use TargetPathTrait;

    private $entityManager;
    private $router;
    private $csrfTokenManager;
    private $passwordEncoder;

    public function __construct(EntityManagerInterface $entityManager, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->entityManager = $entityManager;
        $this->router = $router;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
    }

    public function supports(Request $request)
    {
        return 'security_login' === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        $credentials = [
            'username' => $request->request->get('username'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['username']
        );

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        $user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => $credentials['username']]);

        if (!$user) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('Username could not be found.');
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        return new RedirectResponse($this->router->generate('homepage'));
    }

    protected function getLoginUrl()
    {
        return $this->router->generate('security_login');
    }
}

Une fois cette classe réalisée et correctement implémentée, passons à la création de notre classe Utilisateur.

Création d'utilisateurs et persistance en base

Il ne vous aura pas échappé que le but du formulaire de connexion est de retrouver un utilisateur à partir des informations soumises par l'utilisateur.

Une seule contrainte pour la création d'un utilisateur est d'implémenter l'interface UserInterface du composant Security. Voici un exemple d'implémentation valide utilisé dans le projet de démonstration :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @ORM\Table(name="zozor_user")
 *
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank()
     */
    private $fullName;

    /**
     * @ORM\Column(type="string", unique=true)
     * @Assert\NotBlank()
     * @Assert\Length(min=2, max=50)
     */
    private $username;

    /**
     * @ORM\Column(type="string", unique=true)
     * @Assert\Email()
     */
    private $email;

    /**
     * @ORM\Column(type="string")
     */
    private $password;

    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];

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

    public function setFullName(string $fullName): void
    {
        $this->fullName = $fullName;
    }

    public function getFullName(): string
    {
        return $this->fullName;
    }

    public function getUsername(): string
    {
        return $this->username;
    }

    public function setUsername(string $username): void
    {
        $this->username = $username;
    }

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

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

    public function getPassword(): string
    {
        return $this->password;
    }

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

    public function getRoles(): array
    {
        $roles = $this->roles;

        // il est obligatoire d'avoir au moins un rôle si on est authentifié, par convention c'est ROLE_USER
        if (empty($roles)) {
            $roles[] = 'ROLE_USER';
        }

        return array_unique($roles);
    }

    public function setRoles(array $roles): void
    {
        $this->roles = $roles;
    }

    public function getSalt(): ?string
    {
        return null;
    }

    public function eraseCredentials(): void
    {
    }
}

Configurez la méthode d'authentification

Il est temps de retourner dans le fichier security.yaml pour déclarer notre authenticator, d'une part, et configurer le fournisseur d'utilisateur (le provider) pour utiliser Doctrine ORM, d'autre part.

Voici le fichier security.yaml mis à jour :

# config/security.yaml

security:
    encoders:
        App\Entity\User: bcrypt

    providers:
        database_users:
            entity: { class: App\Entity\User, property: username }

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            pattern: ^/
            anonymous: true
            logout:
                path: security_logout
            guard:
                authenticators:
                    - App\Security\FormLoginAuthenticator

Et après tant d'efforts, et après avoir créé un utilisateur en base de données, vous pouvez maintenant vous connecter :

Capture d'écran est correctement authentifié et a le rôle ROLE_USER
John est bien authentifié !

L'utilisateur John est donc authentifié et aura les rôles définis pour cet utilisateur, que ce soit en base de données, en mémoire, ou toute autre méthode que vous pouvez définir en créant votre propre "User Provider", ou encore fournisseur d'utilisateurs.

Récupérez son utilisateur

Une fois authentifié, on peut donc accéder à son utilisateur n'importe où dans nos applications, que ce soit dans nos services, dans nos contrôleurs et dans nos vues.

Accédez à l'utilisateur dans vos contrôleurs

Dans les contrôleurs, une fois authentifié, il est possible d'accéder à l'utilisateur à l'aide de la fonction getUser() :

<?php

class TestController extends AbstractController
{
    public function index()
    {
        dump($this->getUser()->getUsername());
    }
}

La fonction getUser() est disponible dans AbstractController et retourne soit nul, soit l'utilisateur authentifié.

Si, pour une raison particulière, vous ne souhaitez pas utiliser cet alias, on peut injecter le service Security dans l'action :

<?php

use Symfony\Component\Security\Core\Security;

class TestController extends AbstractController
{
    public function index(Security $security)
    {
        dump($security->getUser());
    }
}
Accédez à l'utilisateur courant dans vos gabarits

Accéder à l'utilisateur courant dans vos gabarits Twig est très facile, à l'aide de la variable globale app.

Voici un exemple d'utilisation pour vos gabarits :

{{ app.user.username }}

Avec toutes ces informations, vous devriez être capable de développer une page "Compte utilisateur" sans aucun souci ! :ange:

Créez un espace administrateur

Nous avons maintenant des utilisateurs qui peuvent s'authentifier et qui ont différents rôles.

L'idée d'un espace administrateur est de restreindre l'accès aux utilisateurs qui ont le rôle  ROLE_ADMIN .

Mise en place de l'accès privé

Une première solution est d'utiliser l'annotation @IsGranted() au niveau des contrôleurs concernés par la partie /admin :

<?php

namespace App\Controller\Admin;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Controller used to manage blog contents in the backend.
 *
 * @Route("/admin/post")
 * @IsGranted("ROLE_ADMIN")
 */
class BlogController extends AbstractController
{
    // ...
}

L'annotation IsGranted attachée au niveau du contrôleur permet de vérifier que l'utilisateur authentifié a bien le rôle  ROLE_ADMIN; sinon, une exception de type  AccessDeniedHttpException  sera lancée.

Une deuxième solution est d'utiliser un concept que vous avez déjà vu dans la première partie de ce chapitre : définir une règle de contrôle d'accès.

Mettons à jour le fichier security.yaml et ajoutons une règle dans la section access_control :

    access_control:
        - { path: '^/admin', roles: ROLE_ADMIN }

Je vois parfois, dans des applications, l'annotation @Security(), pourquoi tu n'en parles pas ?

Cette annotation est dépréciée et ne sera plus disponible dans Symfony 5 ! L'annotation @IsGranted() a les mêmes fonctionnalités, donc vous pouvez convertir tous les appels à l'annotation @Security() par @IsGranted().

Contrôle des droits d'un utilisateur

Nous avons vu comment contrôler l'accès d'un utilisateur à un contrôleur, ou à une plage d'URL, à l'aide du contrôle d'accès dans la configuration de la sécurité de nos applications.

On peut aussi affiner les différentes autorisations au niveau d'une action, d'un service ou même d'un gabarit Twig.

Contrôle des autorisations dans un gabarit

Par exemple, imaginons que nous souhaitons afficher un lien d'édition d'un article du blog seulement si l'utilisateur est administrateur :

{% if is_granted('ROLE_ADMIN') %}
    <div class="section">
        <a  href="{{ path('admin_post_edit', {id: post.id}) }}">
            <i class="fa fa-edit" aria-hidden="true"></i> {{ 'action.edit_post'|trans }}
        </a>
    </div>
{% endif %}
Contrôle des autorisations dans un contrôleur

Nous avons vu comment utiliser l'annotation IsGranted(), car vous pourriez avoir envie de contrôler l'accès à une action du contrôleur seulement si l'on rentre dans une condition particulière de cette action. Puisque l'annotation est exécutée avant d'entrer dans l'action (dans la fonction), on peut utiliser la fonction  denyAccessUnlessGranted($role) :

<?php

use Symfony\Component\Security\Core\Security;

class ExempleController extends AbstractController
{
    public function index()
    {
        //...
        if ($someConditions) {
            $this->denyAccessUnlessGranted('ROLE_SPECIFIC');
        }
    }

    // en utilisant l'injection de dépendances
    public function show(Security $security)
    {
        if ($someConditions) {
            $security->isGranted('ROLE_SPECIFIC');
        }
    }
}

D'accord, c'est très clair... mais j'ai un besoin un peu particulier ! Je veux par exemple permettre à un utilisateur d'éditer un article s'il est le créateur de son article. Je ne vais pas créer un rôle à chaque fois, si ? :colere:

Mais non, ne vous inquiétez pas ! Le contrôle de la sécurité en fonction d'un rôle, c'est le premier niveau de permission implémenté par le composant Security. Le second niveau,  c'est le contrôle d'une action propre à un objet. Je vous explique. ;)

Mise en place de règles complexes

Reprenons l'exemple précédent avec les connaissances que vous avez déjà. Nous pourrions déjà écrire le code suivant dans notre contrôleur :

<?php

use App\Entity\Article;
// ...

class ArticleController extends AbstractController
{
    public function edit(Article $article)
    {
        if ($this->getUser() !== $article->getAuthor() || !$this->isGranted('ROLE_ADMIN')) {
            throw $this->createAccessDeniedException();
        }
    }
}

Et ce code fonctionne parfaitement. Mais... nous avons de la logique business à réécrire dans le contrôleur, dans les gabarits Twig, et si les règles changent ou si elles sont dynamiques, cela va vite devenir compliqué à maintenir !

Pourtant la solution existe, regardez le code de ce contrôleur mis à jour :

<?php

use App\Entity\Article;
// ...

class ArticleController extends AbstractController
{
    public function edit(Article $article)
    {
        if (!$this->isGranted('EDIT', $article)) {
            throw $this->createAccessDeniedException();
        }
    }
}

Sérieux ? C'est tout ? :o

Pas tout à fait, nous allons devoir déplacer nos règles de décision métier dans une classe dédiée appelée un Voteur.

Créez un Voteur pour implémenter des règles spécifiques

Un Voteur, c'est une classe dont la responsabilité est de voter si une action sur un objet est autorisée.

Pour créer un Voteur, le plus simple est d'étendre la classe Voter du composant Security.

Il faudra implémenter deux fonctions principales :

  • supports($attribute, $subject) : définit si le Voteur doit voter ou non sur cette demande d'autorisation ;

  • voteOnAttribute($attribute, $subject, $token): cette fonction doit retourner  true  ou  false; c'est ici que l'on peut décrire les conditions métiers qui conditionnent l'autorisation.

Voici comment nous pourrions utiliser un Voter pour implémenter notre besoin :

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

class ArticleEditionVoter extends Voter
{
    private $security;
    
    public function __construct(Security $security)
    {
        $this->security = $security;
    }
    
    protected function supports($attribute, $subject)
    {
        return $attribute === 'EDIT'
            && $subject instanceof App\Entity\Article;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        // on retrouve l'utilisateur (on peut aussi ré-utiliser $this->security)
        $user = $token->getUser();

        // si l'utilisateur n'est pas authentifié, c'est non!
        if (!$user instanceof UserInterface) {
            return false;
        }

        // l'utilisateur est l'auteur de l'article
        if ($user === $subject->getAuthor()) {
            return true;
        }

        // l'utilisateur est un administrateur
        if ($this->security->isGranted('ROLE_ADMIN')) {
            return true;
        }

        return false;
    }
}

Dans ce Voteur, j'ai eu besoin de vérifier que l'utilisateur avait bien le rôle  ROLE_ADMIN , j'ai donc injecté le service Security dont nous avons parlé précédemment. :soleil:

Ce Voteur est automatiquement configuré par Symfony, si vous n'avez pas désactivé l’autoconfiguration, donc ce code est fonctionnel !

Mettons à jour le code du gabarit Twig :

{% if is_granted('EDIT', post) %}
    <div class="section">
        <a  href="{{ path('admin_post_edit', {id: post.id}) }}">
            <i class="fa fa-edit" aria-hidden="true"></i> {{ 'action.edit_post'|trans }}
        </a>
    </div>
{% endif %}

Enfin, voici le code du contrôleur avec l'utilisation de l'annotation IsGranted() mise à jour :

<?php

use App\Entity\Article;
// ...

class ArticleController extends AbstractController
{
    /**
     * @IsGranted("EDIT", subject="article")
     */
    public function edit(Article $article)
    {
        // ...
    }
}

Avec le système des Voteurs et la fonction isGranted(), vous pouvez donc contrôler l'accès aux différentes actions de votre site, quelle que soit la complexité de vos règles métiers.

Et en bonus, en environnement de développement, le profiler de sécurité est capable de vous dire quel ou quels Voteurs ont participé au vote. C'est utile si vous ne comprenez pas pourquoi un utilisateur n'a pas accès à une action, par exemple.

En résumé

Ça y est ! Grâce à ce chapitre, vous savez à présent mettre en place un espace membre pour votre site. Vous êtes capable de construire un système d'authentification efficace qui garantit la sécurité de votre application. De cette façon, vous pouvez restreindre l'accès à certaines pages en exigeant des droits spécifiques à vos visiteurs pour l'afficher.

Mais vous pouvez aller beaucoup plus loin concernant la sécurité avec Symfony ! Si vous le souhaitez, je vous invite à consulter la documentation officielle de Symfony. Vous pouvez aussi utiliser la documentation de MakerBundle, le bundle communautaire, qui permet d'obtenir très rapidement un espace utilisateur complet.

ll y a deux parties à distinguer dans le processus de sécurité

  • l'authentification, permet d'identifier l'utilisateur :

  • l'autorisation, vérifie son identité et autorise, ou non, l'accès au contenu demandé.

Grâce au fichier de configuration security.yaml, vous pouvez paramétrer chaque élément de la sécurité, notamment :

  • le firewall pour l'authentification

  • le contrôle d'accès pour l'autorisation, en appliquant des règles sur des URL ou les méthodes des contrôleurs, selon vos besoins.

Vous pouvez définir des rôles pour les visiteurs pour établir des droits d'accès. Nous pouvons contrôler les autorisations d'un utilisateur authentifié partout dans nos applications :

  • dans nos contrôleurs ;

  • dans nos vues ;

  • dans nos services...

et de façon très fine à l'aide du système de "Voteurs".

Dans la prochaine partie de ce cours, vous apprendrez comment développer une application de qualité et la déployer en production.

À tout de suite !

Exemple de certificat de réussite
Exemple de certificat de réussite