Couverture de l'article Kit de survie : Symfony2, Gestion utilisateur sans FOSUserBundle
Retour aux articles

L'agence

WanadevStudio

Kit de survie : Symfony2, Gestion utilisateur sans FOSUserBundle

Il y a 12 ans

La gestion utilisateur est souvent le cœur d'une application web. Voici un kit de survie pour apprendre à le faire avec Symfony2, de façon native !

Tout développeur Symfony commence par gérer ses utilisateurs grâce à des bundles dédiés à la gestion  d'utilisateurs comme le fait le très bon bundle FriendsOfSymfony/UserBundle.  Il est particulièrement apprécié par la communauté Symfony tout comme l'organisation qui est en charge de son développement. L’outil est d'ailleurs recommandé sur la page de Symfony qui consacre de la documentation sur la gestion des utilisateurs.

La sécurité est souvent une partie sensible, et nous n'osions (peut-être) pas mettre les mains dans le code jusqu'à il y a encore quelques temps, peut-être entre autres parce que la documentation n'était pas franchement détaillée... Je me suis donc récemment lancé à la découverte de la gestion des utilisateurs de façon "native" car nous avions à travailler sur un projet Symfony2 aux spécifications particulières sur ce point : il n'en fallait pas plus pour me motiver à aller « gratter » dans l’api Symfony.

Suite à cette expérience, je vais vous présenter les quelques interfaces et configurations à réaliser pour mettre en place une sécurisation de votre site rapidement sans avoir à faire appel à un bundle externe.

La vie sans FOSUserBundle

Mettre en place notre entité User

La première étape est de sélectionner la classe qui sera responsable de gérer les utilisateurs. Dans la plupart des cas, cette entité s'appellera « User » mais vous pourriez utiliser d'autres noms d'entités. Vous verrez par la suite que cela n'a aucune importance pour Symfony : vous pourriez utiliser comme "utilisateur" une entité "entreprise", "vendeur" (ou même "légume").

Admettons que nous voulons que les utilisateurs de notre site se connectent avec l'entité User, ce qui demeure une base.

class User implements Symfony\Component\Security\Core\User\UserInterface
{
    private $id;
    private $name;
    private $email;
}

Ici, nous avons notre classe qui possède tous les champs de tous types que vous souhaitez utiliser. Indépendamment des informations que vous souhaitez garder rattachées à votre utilisateur, vous devez implémenter l'interface UserInterface de Symfony.

interface UserInterface
{
    public function getRoles();
    public function getPassword();
    public function getSalt();
    public function getUsername();
    public function eraseCredentials();
}

L'interface est assez simple à implémenter. Je vous ai copié les prototypes des méthodes sans leur documentation. Je vous invite donc à aller vous documenter sur cette petite interface pour la comprendre intégralement.

Voici une implémentation possible.

class User implements Symfony\Component\Security\Core\User\UserInterface
{
    private $id;
    private $name;
    private $email;
    private $username;
    private $roles;
    private $password;
    private $salt;

    public function __construct() {
        // De base, on va attribuer au nouveau utilisateur, le rôle « ROLE_USER »
        $this->roles = array("ROLE_USER");
        // Chaque utilisateur va se voir attribuer une clé permettant 
        // de saler son mot de passe. Cela n'est pas obligatoire,
        // on pourrait mettre $salt à null
        $this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
    }

    public function getRoles() {
        return $this->roles;
    }

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

    public function getSalt() {
        return $this->salt;
    }

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

    public function eraseCredentials() {
        // Ici nous n'avons rien à effacer. 
        // Cela aurait été le cas si nous avions un mot de passe en clair.
    }
}

Mettre en place un firewall

La prochaine étape est de déclarer un firewall. Simplifions pour ceux qui auraient dû mal à identifier quel est le but de ce firewall. Il permet de déclarer un contexte dans lequel certaines routes sont publiques ou privées. Cela lui permet de gérer de manière indépendante la page d'authentification, les redirections, la gestion de la session…

Conventionnellement, les firewalls et toutes les informations globales de votre application sont déclarées dans le fichier security.yml que vous trouverez dans le dossier app/config.

Ce fichier contient quatre parties distincts et complémentaires.

Comment déclarer un provider

La première étape est de déclarer l'entité qui devra être utilisée par le provider de votre ou de vos contextes.

# app/config/security.yml
security:
    providers:
        main:
            entity: { class: MyNamespace\MyBundle\Entity\User, property: username }

Dans notre déclaration, nous avons attribué pour le provider « main » notre entité. Nous lui avons donné la classe ainsi que la propriété qui permettra à l'utilisateur de se connecter. Ici, il s'agit de « username », mais cela pourrait être n'importe quel champs disposant, de préférence, d'un minimum d'unicité (Deux utilisateurs ne devront pas avoir le même login, par exemple).

Dans le cas où vous souhaiteriez utiliser un provider qui autoriserait l'authentification via l'email ou un username, vour devrez implementer un user provider.

Déclarer l'encodage des mots de passe

Dans cette partie, nous allons déclarer le type d'encodage des passwords pour notre entité.

# app/config/security.yml
security:
    encoders:
        MyNamespace\MyBundle\Entity\User:
            algorithm: sha512
            iterations: 9616
            encode_as_base64: true

Voici comment vous devrez déclarer l'encodage. C'est très simple : vous disposez de plusieurs éléments pour configurer cela.

  • algorithm : De base il est déterminé avec sha512, mais vous pouvez changer cette valeur avec un algorithme différent ou même indiquer « plainText » (dans le malheureux cas où votre application aurait hérité de mot de passe en clair… À éviter au maximum bien évidemment.)
  • iterations : Cela permet de déterminer le nombre de fois que le mot de passe sera encodé avant d'être comparé avec la base de données. Par défaut, la valeur est de 5000. Si vous en avez la possibilité, je vous conseille de la modifier et de mettre une valeur aléatoire supérieure. En effet, une personne qui essayera de brute-force votre système de connexion essayera par défaut  de faire des comparaisons avec sa base de mot de passe, établie avec 5000 itérations. Avec ne serait-ce qu'une itération supplémentaire (soit 5001), l'intégralité de la base du brute-forceur deviendra incorrecte.
  • encode_as_base64 : le troisième et dernier paramètre consiste à indiquer si le mot de passe, une fois encodé en suivant le nombre d'itérations demandées, sera encodé en base 64. De base c'est le cas.

Déclaration du firewall

Le dernier point essentiel est de déclarer un firewall. Voici la configuration que vous pourrez trouver sur le site de Symfony.

# app/config/security.yml
security:
    firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false

    login:
        pattern: ^/login$
        security: false

    main:
        pattern: ^/
        form_login:
            login_path: login
            check_path: login_check
        anonymous: true

        logout:
            path: /logout
            target: /

        remember_me:
            key: "%secret%"
            lifetime: 2232000

Je vais vous décrire de manière brève les quelques lignes ci-dessus. Déjà, vous pouvez remarquer que nous avons non pas 1, mais 3 firewalls.

  • dev Ce firewall n'est pas utile lorsque vous êtes dans une application en production. Cela ne s'applique uniquement lorsque vous développez avec « app_dev.php » par exemple. Il est donc quand même nécessaire pour afficher la debug bar de Symfony.
  • login Ce firewall est essentiel. Ce que nous avons déclaré indique que pour la route correspondant exactement à « /login », nous ne devons pas appliquer de contexte de sécurité. N'importe qui peut accéder à la page de connexion. (Ça serait quand même dommage de ne pas pouvoir accéder à votre page de connexion sans être loggué...n'est-ce pas ?)
  • main Et voici la configuration pour votre application. Dans notre cas ainsi que dans nombreux autres, vous n’aurez pas besoin de déclarer d'autres « vrais » firewall que celui-ci. Il déclare que pour toutes les routes commencement par « / » (toutes les routes), j'applique ce firewall. Si je ne suis pas connecté et que la route à laquelle j'accède ne requiert pas nécessairement d'authentification, un token « anonymous » me serait tout de même donné. (visible dans la debug bar de Symfony2)

Dans le cas où j'essaierai d'accéder à une page qui requiert un rôle que je dispose pas (par exemple : être connecté), je serai automatiquement  redirigé vers le « login_path » indiqué. Enfin, le « check_path » est la route permettant de vérifier les données d'identification envoyées.

Pour finir, les paramètres « remember_me » permettent de régler l'option « se souvenir de moi » en session.

Quatrième partie : gestion des contrôles d'accès

La gestion de la sécurité est disponible de plusieurs manières (annotation, php, xml, yaml) mais également depuis le configuration pour déterminer des règles plus générales. Pour éviter de créer une faille dans l'espace temps, nous n'ouvrirons pas un nouveau débat sur l’infatigable sujet sur la meilleure façon de configurer la sécurité.

Voici une configuration simple.

# app/config/security.yml
security:
    access_control:
        - { path: ^/private/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/private, roles: ROLE_ADMIN }
        - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }

Attention, lorsque vous serez connecté, si la route sur laquelle vous êtes ne correspond à aucun des patterns présents dans « access_control », vous aurez probablement dans la debug bar de Symfony, une pastille orange sur l'emplacement permettant de donner les informations d'identification (token).

Générer un controlleur

Désormais, votre application est configurée mais il vous manque tout de même un contrôleur vous permettant de gérer la connexion.

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class SecurityController extends Controller
{}

Vous devrez ensuite générer trois méthodes login, login_check et logout.

Login

Cette méthode est la seule que vous allez vraiment devoir remplir.Le code que je présente n'est pas le mien, il s'agit de celui de Symfony. Une implémentation légèrement différente dans FOSUserBundle est aussi intéressante et fait appel à un form token.

/**
* @Method({"GET"})
* @Route("/login", name="login")
* @Template()
*/
public function loginAction(Request $request)
{
    $request = $this->getRequest();
    $session = $request->getSession();

    if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
        $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
    } else {
        $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
        $session->remove(SecurityContext::AUTHENTICATION_ERROR);
    }

    $params = array(
        "last_username" => $session->get(SecurityContext::LAST_USERNAME),
        "error"         => $error,
    );

    return $params;
}

Pour finir, voici le template dont vous aurez besoin pour connecter vos utilisateurs (source: symfony.com).

<form action="{{ path("login_check", {}) }}" method="POST" >
    <div class="form-group">
        <label for="inputUsernameEmail">Username</label>
        <input type="text" required="required" class="form-control" value="{{ last_username }}" name="_username" id="username">
    </div>

    <div class="form-group">
        <label for="inputPassword">Password</label>
        <input type="password" class="form-control" name="_password" required="required" id="password">
    </div>

    <div class="checkbox pull-left">
        <label>
        <input type="checkbox" id="remember_me" name="_remember_me" value="on" />
        <label for="remember_me">Remember me</label>
        </label>
    </div>

    <button type="submit" class="pull-right btn btn btn-primary">
        Log In
    </button>
</form>

Login check et logout

Pour les deux méthodes login_check et logout, vous devez uniquement déclarer la route. Si vous avez configuré votre application correctement, vous ne devriez jamais entrer dans les méthodes car les requêtes devraient être interceptées avant même que le contrôleur n'attrape la requête.

/**
* @Method({"POST"})
* @Route("/login_check", name="login_check")
*/
public function check()
{
    throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
}

/**
* @Method({"GET"})
* @Route("/logout", name="logout")
*/
public function logout()
{
    throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
}

Vous possédez désormais les outils basiques permettant de connecter et d'assurer une certaine sécurité pour votre plateforme et vos utilisateurs. Sur une plateforme, nous avons besoin d'autres éléments pour que notre site soit complet, comme donner la possibilité d’enregistrer des utilisateurs, mettre en place une fonctionnalité de « reset password », changer son mot de passe et mettre une validation d'un compte utilisateur.

Photo de Baptiste DONAUX auteur de l'article

Baptiste DONAUX