L'agence
WanadevStudio
Kit de survie : Symfony2, Gestion utilisateur sans FOSUserBundle
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.
Commentaires
Julien Lgr
Il y a 10 ans
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.
Baptiste DONAUX
Il y a 10 ans
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
Julien Lgr
Il y a 10 ans
Effectivement en utilisant un Provider perso ça marche de suite bien mieux. Merci beaucoup !
rhuduweb
Il y a 9 ans
cdt.
Baptiste DONAUX
Il y a 9 ans
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
Hugo
Il y a 9 ans
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 !
Baptiste DONAUX
Il y a 9 ans
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
Hugo
Il y a 9 ans
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.
Baptiste DONAUX
Il y a 9 ans
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
Hugo
Il y a 9 ans
C’était bien mon problème, merci !
Bonne continuation,
Hugo
Marwa
Il y a 9 ans
Baptiste DONAUX
Il y a 9 ans
Lionnel
Il y a 9 ans
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
Stéphane
Il y a 8 ans
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".)
sand
Il y a 7 ans
As-tu resolu ton problème stp ? j'en ai le meme
Badr
Il y a 8 ans
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
Baptiste DONAUX
Il y a 8 ans
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
Baptiste Pottier
Il y a 8 ans
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 ;-) ...
François DELEGLISE
Il y a 8 ans
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) ;-) !
Baptiste DONAUX
Il y a 8 ans
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 !
noel kenfack
Il y a 10 ans
Baptiste DONAUX
Il y a 10 ans
gaylord
Il y a 10 ans
Baptiste DONAUX
Il y a 10 ans
Bonjour,
Tu peux ajouter des relations et des attributs comme dans n’importe quelle entité.
Dans ton cas, pour établir une relation entre un utilisateur et ses articles, tu devrais utiliser une relation classique de type OneToMany (un utilisateur peut avoir rédigé plusieurs articles, mais un article n’a qu’un seul auteur).
Voici la documentation si tu utilises Doctrine => http://doctrine-orm.readthedocs.org/en/latest/reference/association-mapping.html#one-to-many-self-referencing