Kit de survie : Symfony2, Gestion utilisateur sans FOSUserBundle

Difficulté : | 20' Publié il y a 3 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.

Tags de l'article :

Cet article n'est pas taggué.

Commentaires

  • Il y a 3 ans noel kenfack : Répondre

    je n’arrive par à connecté mes utilisateurs et je voulais savoir pourquoi vous avez mis ne rien effacer dans lea méthode eraseCredentials. chez moi cette méthode ne contient aucun code.

    • Il y a 3 ans Baptiste

      Bonjour, La méthode eraseCredential permet d’effacer des données dites sensibles. Exemple, dans le cas d’une base de données qui stockerait les mots de passe en clair, une fois l’utilisateur authentifié on effacerait le mot de passe de manière à ce qu’il ne soit pas visible. Dans la présentation, cette méthode est vide car le mot de passe de notre utilisateur est crypté. Voila :)

  • Il y a 2 ans gaylord : Répondre

    Est-ce que c’est possible de relier la table user créée avec fosuserbundle à une autre table d’un autre bundle? Exemple, comment je fais pour mettre en relation un utilisateur avec la table des articles qu’il a publiés

  • Il y a 2 ans Julien Lgr : Répondre

    Bonjour,

    J’essaye votre solution, mais je rencontre l’erreur suivante : « The class ‘XXX’ was not found in the chain configured namespaces ».

    Une idée ? Sachant qu’à priori le namespace est correct. Par contre je n’ai pas mappé l’entité avec Doctrine. Vu que je vais utiliser un webservice pour me connecter. Donc il n’y aura pas de bdd sur mon application.

    J’avoue que je tourne un peu en rond. Merci d’avance si vous avez un tuyau/une hypothèse.

    • Il y a 2 ans Baptiste

      Bonjour,

      En réfléchissant un peu au problème que tu rencontres voici ce que je pense avoir analyé.
      L’erreur qui t’es levée « The class ‘XXX’ was not found in the chain configured namespaces » est une erreur levée par Doctrine. En effet par défaut, Symfony utilise Doctrine via le provider par défaut. Cette erreur s’explique donc par le fait que tu n’as pas déclaré de mapping Doctrine.

      Par conséquent, je pense que tu parfaitement réalisé ce tutoriel, mais ton problème se trouve dans l’implémentation de ton propre Provider. Il te permettra d’effectuer tes propres appels à ton webservice et d’hydrater ta propre entité User.

      Tu peux trouver un tutoriel sur Comment implémenter son propre UserProvider.

      N’hésites pas à revenir vers nous pour nous dire si cette réponse a pu t’apporter une solution viable.

      Cldt, Baptiste

    • Il y a 2 ans Julien Lgr

      Bonjour et merci pour ta réponse.

      Effectivement en utilisant un Provider perso ça marche de suite bien mieux. Merci beaucoup !

  • Il y a 2 ans rhuduweb : Répondre

    Bonjour, j’aimerai me passer du bundle fosuserbundle dans symfony mais pouvoir gérer l’inscription d’un internaute sans qu’il n’entre de mot de passe juste son email car c’est l’admin qui lui envoie par mail un mdp crypté pour qu’il se connecte (envoi avec swiftmailer) est-ce possible car je ne trouve aucun tuto sur le net pour faire cela. merci si vous avez une solution.
    cdt.

    • Il y a 2 ans Baptiste

      Bonjour,

      Ta démarche va dans le bon sens. De plus, le cadre très métier de ton application fait que gérer toi-même cette partie te facilitera la tâche.

      Sache que créer un utilisateur sans qu’il ne renseigne de mot de passe est facile. Il suffit que tu implémentes ton SecurityController. Comme le code n’est pas donné ici, tu peux te rendre sur ce tutoriel (Implémenter son propre SecurityController : http://www.baptiste-donaux.fr/securitycontroller-implementation/) et en extraire le code du contrôleur. Tu pourras alors facilement enlever la partie gestion du mot de passe de ton formulaire et de ton contrôleur.

      Malgré tout, attention à ta stratégie de création de mot passe. Ta solution peut comporter quelques failles :
      – L’administrateur enverra un mail avec le mot de passe crypté.
      Le mot de passe crypté est inutilisable par qui que ce soit. De plus, l’intervention d’une personne peut être source d’erreurs et fails. Mettre en place un service générant aléatoirement un mot de passe est préférable.
      – Pour finir, l’envoi de mot de passe en « plain text » est monnaie-courante. Cependant, il est aujourd’hui conseillé d’envoyer un lien par mail, et permettant à l’utilisateur de choisir un mot de passe.

      N’hésites pas à revenir vers nous pour nous dire si cette réponse a pu t’apporter une solution viable.

      Cordialement, Baptiste Donaux

  • Il y a 2 ans Hugo : Répondre

    Bonjour,
    Après avoir suivi le déroulement du tutoriel ci-dessus, je n’arrive toujours pas à gérer le « access_control » & la définition pour un utilisateur de son rôle. J’ai cherché un peu partout mais je n’ai toujours pas la solution à ce problème… Serait-ce l’utilisation de mon propre provider ? Merci d’avance si quelqu’un arrive à trouver une solution !

    • Il y a 2 ans Baptiste

      Bonjour Hugo,

      Le fait qu’une entité soit un utilisateur utilisable via une connexion et un token tient de l’implémentation de UserInterface. Si tu peux te connecter avec un utilisateur alors c’est que tu as implémenté d’une quelconque manière cette interface.

      Dans cette interface, la méthode getRoles a pour but de retourner les rôles de ton utilisateur en cours. En général, lorsqu’on veut les droits des utilisateurs via une base de données, on associe cette méthode à un champs en base (de type json_array).

      Le access_control se situe sous l’item security (souvent présent dans le fichier app/config/security.yml). Les rôles que tu appliques ici sont testés via un voter interne (implémentation VoterInterface). Ce voter match les rôles de ton utilisateur avec celui demandé.

      Ce que je peux te conseiller, c’est de vérifier que :
      – Les routes que tu essaies de faire fonctionner dans ton access_control matchent correctement.
      – Que ton utilisateur possède les bons rôles (et petit var_dump et puis s’en va…)

      Pour finir, à moins d’une utilisation abusive ou détournée de ton provider, celui-ci ne doit pas interagir sur la gestion des rôles et encore moins sur la validation des droits de tes utilisateurs.

      N’hésites pas à revenir vers nous si nécessaire et à parler de nous (ou de notre article

    • Il y a 2 ans Hugo

      Bonsoir,
      Tout d’abord, merci de me répondre si rapidement, j’ai bien vérifié ces deux aspects et ils ont opérationnels.

      Néanmoins, nouveau petit problème: Lorsque j’essaie de me connecter via mon formulaire de connexion, il me renvoie le message d’erreur « Bad Credentials » affiché par mon template, même si je lui rentre le bon couple login/mot de passe de mon utilisateur en base de donnée, ayant le bon rôle pour y accéder. Je pense que mon formulaire de connexion ne fait aucune vérification , et m’envoie par défaut ce message d’erreur.

      En vous remerciant d’avance.

    • Il y a 2 ans Baptiste

      Bonjour Hugo.

      Généralement on pense que ce type d’erreur est une erreur de « workflow » mais il n’en ai rien ! Si tu saisies le bon login/mot de passe et que tu reçois cette erreur, c’est que la manière dont ton mot de passe est vérifié n’est pas bonne. Cela peut (par exemple) venir de l’encodeur. N’hésites pas à recharger tes fixtures pour vérifier que l’encodage de tes mots de passe en base de données a correctement été réalisé.

      Cordialement, Baptiste

    • Il y a 2 ans Hugo

      Bonjour,
      C’était bien mon problème, merci !

      Bonne continuation,
      Hugo

  • Il y a 2 ans Marwa : Répondre

    bonjour, J’essaye votre solution mais j'ai une erreur lorsque j'ai entré un login et un pwd :'Notice: unserialize(): Error at offset 0 of 9 bytes'. Pouvez vous m'aider et merci d'avance.

    • Il y a 2 ans Baptiste

      Bonjour, Cela semble être un soucis sur la sérialisation de ton entité. Ton dois probablement avoir dans ton entité un objet non sérialisable (comme un UploadedFile par exemple). Cordialement, Baptiste

  • Il y a 1 an Lionnel : Répondre

      Bonjour Baptiste, Merci infiniment pour ce tuto.              

     En fait j'aimerai savoir si possible comment  faire pour implémenter un provider password  enfin d'utiliser uniquement un mot de passe lors de la connexion

  • Il y a 8 mois Stéphane : Répondre

    Bonsoir,

    J'ai la même erreur que marwa :

    Notice: unserialize(): Error at offset 0 of 10 bytes

    J'utilise Doctrine et je n'ais pas de UploadedFile.

    Si vous pouvez m'orienter, je vous en serais bien reconnaissant.

    ( Ps : le lien "documenter" à propos des interfaces est mort".)

      


  • Il y a 6 mois Badr : Répondre

    Salut,
    Merci pour ce tuto,
    Pourriez vous expliquer un peu plus quells sont ces spécifications particulières sur la gestion des utilisateurs ?
    quand est ce que il faut la faire d'une facon native ? et pourquoi ne pas utiliser FosUserBundle ?
    Merci d'avance

    • Il y a 6 mois Baptiste

      Bonjour

      FOSUserBundle est une implémentation des composants natifs de Symfony. FOSUserBundle propose des fonctionnalités déjà développées comme la réinitialisation de mot de passe et autres. L'intérêt de FOS est de ne pas implémenter la brique authentification mais cela a l'inconvénient de donner moins de flexibilité dans les cas moins standards.

      Cordialement, Baptiste

  • Il y a 5 mois Baptiste Pottier : Répondre

    Hello,

    Très bien tes articles ! Juste une petite remarque constructive sur le dernier lien de ton article, une coquille dans le href : http://www.baptiste-donaux.fr/securitycontroller-implementaion/ comme la page n'existe pas on est redirigé vers ta home, je suppose qu'il manque juste le second "t" de "implementation", encore bravo pour ton boulot basé sur ton d’expérience !

    Et en plus tu as le plus beau prénom du monde ;-) ...

    • Il y a 5 mois François

      Baptiste, merci pour ton commentaire ;-) ! Le lien est désormais corrigé ! À bientôt sur le blog, Twitter, ou dans la salle de jeu pour un café (on te doit bien ça pour avoir trouvé cette coquille) ;-) !

    • Il y a 5 mois Baptiste

      Salut Baptiste !

      En plus d'avoir le plus beau prénom, tu permets désormais aux autres personnes d'avoir un lien cassé en moins !

      Merci pour ce retour, et encore merci pour ton soutien !