sfDoctrineGuardPlugin et l'authentification externe
Date de publication : 30/10/2010. Date de mise à jour : 09/05/2011.
Par
Thibaut Cuvelier (Site Web) (Blog)
symfony est un excellent framework PHP mais il ne dispose pas de base de fonctionnalités avancées de gestion de l'utilisateur,
trop dépendantes de la base de données. Un plug-in est donc là pour pallier ce genre de nécessités, sfDoctrineGuardPlugin.
Il n'est pas forcément des plus simples à prendre en main lorsqu'il s'agit de personnaliser la méthode d'authentification
des utilisateurs, c'est ce manque que vient combler ce tutoriel.
I. Introduction
II. Installation
III. Authentification externe
IV. Remplir automatiquement la table des utilisateurs
V. Mettre à jour les informations des utilisateurs
VI. Gérer les droits
VII. Conclusion
I. Introduction
symfony est un framework PHP assez répandu et utilisé par de grandes firmes, comme Yahoo pour son service
Bookmarks.
Vous pouvez en consulter la liste sur
le site de symfony.
Cependant, la gestion des utilisateurs et de la sécurité n'est pas parfaite ni très poussée dans la version de base du framework. En effet, de telles fonctionnalités
requièrent généralement une base de données, alors que symfony n'est pas prévu pour en avoir besoin pour tous les usages. C'est pourquoi toute cette gestion avancée
a été déportée dans un plug-in,
sfGuardPlugin à la base, pour l'ORM Propel, qui fut présent
dès les débuts de symfony. Cependant, Sensio Labs, éditeur de symfony, a débuté le développement de Doctrine, un autre ORM, maintenant conseillé pour symfony.
Un port du plug-in a donc eu lieu, donnant naissance à
sfDoctrineGuardPlugin.
D'abord un port, celui-ci a continué d'avancer, étant, au moment de la rédaction, à la version 5.0.0, alors que la version Propel est restée à la 4.0.1.
Dans cet article, nous utiliserons symfony en version 1.4.8 et sfDoctrineGuardPlugin en version 5.0.0, les dernières en ce moment. Il devrait rester valable
pour toute la série 1.4.x de symfony et 5.x de sfDoctrineGuardPlugin. Je partirai du principe que vous connaissez les bases de symfony et de la gestion des droits
avec ce framework, un peu l'ORM Doctrine. Il n'est pas nécessaire de déjà connaître sfDoctrineGuardPlugin, nous n'écrirons aucune ligne d'aucun module qui l'appelle directement.
 |
Les deux plug-ins partageant une même base, il est probable que le contenu de ce tutoriel s'applique aisément à sfGuardPlugin. Cela n'est pour autant pas garanti,
vous devrez probablement modifier un peu les fichiers proposés.
|
II. Installation
sfDoctrineGuardPlugin ne peut pas être inclus d'office dans symfony, car il a besoin d'une base de données pour fonctionner. Il est donc nécessaire de l'installer.
Commençons par le télécharger. Plusieurs techniques sont possibles : par la ligne de commande, par checkout Subversion ou manuellement.
| En ligne de commande |
symfony plugin:install sfDoctrineGuardPlugin
|
| Avec un checkout Subversion |
svn co http//svn.symfony-project.com/plugins/sfDoctrineGuardPlugin/trunk plugins/sfDoctrineGuardPlugin
|
Manuellement, vous devrez télécharger l'archive du plug-in
sur sa page et
la décompresser dans le dossier
plugins/sfDoctrineGuardPlugin de votre projet (seul le dossier
sfDoctrineGuardPlugin-5.0.0
contenu dans cette archive nous intéresse, le fichier
package.xml ne servant que pour l'installation en ligne de commande, il est requis par
le module d'installation PEAR que symfony utilise).
Ensuite, il faut activer ce plug-in dans symfony. Pour ce faire, ouvrez le fichier de configuration
ProjectConfiguration.class.php de votre projet (dans le dossier config).
Vous y trouverez une méthode setup() avec, généralement, un plug-in déjà activé, votre ORM. Cette fonction ressemble donc à ceci :
public function setup()
{
$this->enablePlugins('sfDoctrinePlugin');
}
|
Complétez cette fonction avec le nom du plug-in. Deux méthodes sont possibles : soit ajouter un paramètre à enablePlugins, soit ajouter un appel
à cette même méthode.
public function setup()
{
$this->enablePlugins('sfDoctrinePlugin');
$this->enablePlugins('sfDoctrineGuardPlugin');
}
|
public function setup()
{
$this->enablePlugins('sfDoctrinePlugin', 'sfDoctrineGuardPlugin');
}
|
 |
Vérifiez bien que l'ORM Doctrine est bien aussi chargé ! Sinon, sfDoctrineGuardPlugin ne pourra pas fonctionner !
Vérifiez aussi que vous n'utilisez pas Propel, auquel cas il vous faudra utiliser sfGuardPlugin.
|
Maintenant, il faut impérativement régénérer votre modèle. Sinon, vous aurez droit à des erreurs 500 indiquant qu'une classe n'a pas été trouvée
lorsque vous tenterez d'utiliser sfDoctrineGuardPlugin. Pour ce faire, cette ligne dans un prompt devrait suffire :
symfony doctrine:build --all --and-load
|
Notez qu'elle modifiera aussi votre base de données, rendant de fait les modèles utiles.
Ceci clôt la première partie de l'installation, celle qui est commune à tous les plug-ins (ou presque). Maintenant, il faut ajouter les modules
que fournit sfDoctrineGuardPlugin, contenant notamment les formulaires de connexion et les interfaces d'administration auto générées.
Aussi, un peu de configuration supplémentaire est requise pour que le tout fonctionne bien, notamment la fonctionnalité "Se souvenir de moi",
et que symfony utilise bien les formulaires de sfDoctrineGuardPlugin.
Pour toute application de type backend, nécessitant d'éditer les tables du plug-in, activez les modules sfGuardGroup,
sfGuardUser et sfGuardPermission. Éditez donc le fichier settings.yml de votre application comme suit :
all:
.settings:
enabled_modules: [default, sfGuardGroup, sfGuardUser, sfGuardPermission]
|
Par contre, dans votre application frontend, seul sfGuardAuth sera nécessaire pour gérer l'authentification :
all:
.settings:
enabled_modules: [default, sfGuardAuth]
|
Vous pouvez additionnellement ajouter l'option "Se souvenir de moi" en ajoutant dans votre fichier filters.yml ce qui suit :
remember_me:
class: sfGuardRememberMeFilter
|
Maintenant, il faut que symfony pense à utiliser sfDoctrineGuardPlugin pour les formulaires de connexion.
À cette fin, ajoutez ces quelques lignes à votre fichier settings.yml :
login_module: sfGuardAuth
login_action: signin
secure_module: sfGuardAuth
secure_action: secure
|
sfDoctrineGuardPlugin ajoute des fonctionnalités, évidemment, à l'objet utilisateur que vous manipulerez dans votre application.
Pour que ces ajouts puissent avoir lieu, il suffit d'ajouter une classe en parent de votre myUyser :
class myUser extends sfGuardSecurityUser
{
}
|
 |
Si vous ne le faites pas, vous risquez de recevoir une erreur du style Call to undefined method myUser::isAnonymous..
|
Si le module sfGuardAuth est activé et que vous ne demandez pas explicitement lors de la configuration dans le fichier app.yml
que les routes nécessaires ne soient pas crées, deux routes seront automatiquement ajoutées à l'exécution :
sf_guard_signin:
url: /login
param: { module: sfGuardAuth, action: signin }
sf_guard_signout:
url: /logout
param: { module: sfGuardAuth, action: signout }
|
Par défaut, l'action signout renvoie l'utilisateur sur @homepage, vérifiez donc bien qu'une telle route est définie.
Toute dernière étape, définir les applications ou les modules ou les actions à sécuriser. Il suffit de modifier le fichier security.yml
au niveau à sécuriser comme suit pour sécuriser toute l'application ou tout un module :
Vous pouvez activer la sécurisation depuis le fichier security.yml de l'application :
module_a_securiser:
is_secure: true
|
De même, une action peut être sécurisée sans impacter les autres actions du module, à condition d'utiliser ce genre de code
dans le security.yml du module concerné :
action_a_securiser:
is_secure: true
|
III. Authentification externe
sfDoctrineGuardPlugin dispose d'une méthode relativement simple mais limitée de vérification des mots de passe, c'est généralement ce que l'on veut.
Remarquez qu'elle ne fait que changer la méthode de vérification des mots de passe, les colonnes associées du modèle de sfDoctrineGuardPlugin
ne seront juste plus utilisées, vos utilisateurs devront toujours être en base. Nous verrons un peu plus loin comment dépasser cette limitation.
L'implémentation de la fonction est extrêmement simple : à partir d'un nom d'utilisateur et d'un mot de passe donnés, il suffit de renvoyer un booléen dont
la fonction est, ô miracle, de déterminer si le mot de passe associé à l'utilisateur est bien celui attendu. Entre les deux, vous pouvez faire ce que vous voulez :
vous connecter à un annuaire LDAP, vérifier dans une base de données externe... ou renvoyer true dans tous les cas (par exemple, si vous voulez
faire une démonstration d'une application).
Stockez donc cette méthode dans une classe myLogin, qui sera hébergée dans le dossier lib de votre application sous le nom
de myLogin.class.php :
<?php
class myLogin extends sfGuardSecurityUser
{
public static function checkPassword($username, $password)
{
if($username && $password)
return true;
}
}
|
Réimplémentez donc cette méthode pour qu'elle convienne à vos besoins et desiderata.
Maintenant, il faut demander à sfDoctrineGuardPlugin d'utiliser cette méthode pour connecter vos utilisateurs.
Cela se fait très simplement dans le fichier de configuration app.yml de votre application :
all:
sf_guard_plugin:
check_password_callable: [myLogin, checkPassword]
|
Maintenant, étudions un peu ce qui se passe derrière le capot, pourquoi justement cette modification ne suffit pas pour que sfDoctrineGuardPlugin
introduise l'utilisateur en base à la connexion.
Tout se passe dans la classe sfGuardValidatorUser, dans l'une des trois méthodes qu'elle implémente : doClean().
Voici ce qu'elle contient, dans les versions 4.0 du plug-in :
protected function doClean($values)
{
$username = isset($values[$this->getOption('username_field')]) ? $values[$this->getOption('username_field')] : '';
$password = isset($values[$this->getOption('password_field')]) ? $values[$this->getOption('password_field')] : '';
$allowEmail = sfConfig::get('app_sf_guard_plugin_allow_login_with_email', true);
$method = $allowEmail ? 'retrieveByUsernameOrEmailAddress' : 'retrieveByUsername';
if ($username)
{
if ($username && $user = Doctrine::getTable('sfGuardUser')->retrieveByUsername($username))
{
if ($user->getIsActive() && $user->checkPassword($password))
{
return array_merge($values, array('user' => $user));
}
}
}
if ($this->getOption('throw_global_error'))
{
throw new sfValidatorError($this, 'invalid');
}
throw new sfValidatorErrorSchema($this, array($this->getOption('username_field') => new sfValidatorError($this, 'invalid')));
}
|
 |
La version 5.0.0 ajoute une petite subtilité justement au niveau de la vérification de l'existence en base de l'utilisateur.
Cette ligne est remplacée par les suivantes :
if ($username && $user = Doctrine::getTable('sfGuardUser')->retrieveByUsername($username))
|
if ($callable = sfConfig::get('app_sf_guard_plugin_retrieve_by_username_callable'))
{
$user = call_user_func_array($callable, array($username));
} else {
$user = $this->getTable()->retrieveByUsername($username);
}
|
Ce qui laisse présupposer qu'il est possible de surcharger uniquement cette partie. Cette partie est absolument non documentée,
je n'ai trouvé personne qui avait essayé cette méthode ; mes essais n'ayant pas été fructueux, j'ai préféré utiliser une autre méthode
pour arriver à mes fins et oublier cette subtilité.
|
La dernière partie, vérifiant le mot de passe, appelle effectivement votre fonction de vérification. Cependant, auparavant, cette méthode
vérifie que l'utilisateur est en base, ce qui est le comportement le plus normal attendu dans le cas général d'utilisation du plug-in -
l'utilisation de l'authentification de base -, qui n'est pas forcément ce qui est voulu (système de SSO, Intranet, etc.).
IV. Remplir automatiquement la table des utilisateurs
Le problème, c'est que vous n'avez pas forcément toujours tous vos utilisateurs dans votre base. Et que ce système refuse de tenter d'authentifier
des utilisateurs qui n'y sont pas. Que faire ? Réimplémenter un nouveau formulaire qui s'occupe de créer l'enregistrement le cas échéant puis
retourne à l'implémentation de base ? Ce n'est pas la voie choisie ici, j'ai préféré utiliser un validateur personnalisé, s'occupant de toute la
grasse besogne, y compris l'authentification elle-même (ce qui signifie que vous n'aurez plus besoin du code de la partie précédente sous cette forme,
il sera intégré dans le nouveau validateur : vous pourrez très bien y faire appel si vous préférez).
Tout d'abord, il faut utiliser un autre formulaire, héritant de l'actuel mais ajoutant notre validateur. On aurait très bien pu modifier le plug-in lui-même,
cette approche étant évitée car très peu propre et nécessite de remodifier le plug-in à chaque mise à jour. Dans la configuration de l'application
app.yml, notifions symfony qu'il ne faut pas utiliser le formulaire de base :
all:
sf_guard_plugin_signin_form: myAuthForm
|
Créons ensuite ce nouveau formulaire, ajoutant notre validateur (lib/myAuthForm.class.php).
<?php
class myAuthForm extends sfGuardFormSignin
{
public function configure()
{
parent::configure();
$this->validatorSchema->setPostValidator(new sfGuardValidatorCustomUser());
}
}
|
En fait, il ne fait presque rien : tout est délégué au parent, il ne fait qu'ajouter le validateur personnalisé. Implémentons-le maintenant.
Seule la méthode dont le fonctionnement nous ennuyait sera réimplémentée, doClean().
Que faut-il y faire ? Vérifier que l'utilisateur est en base : s'il n'y est pas, il faut l'ajouter avec toutes les informations utiles.
Ensuite, vérifier son mot de passe.
(1)
 |
Ici, le comportement de la fonction de recherche est beaucoup changé et permet aux utilisateurs de changer de nom d'utilisateur. En effet, la méthode
externe nous renvoie un identifiant qui, lui, ne changera jamais et identifiera toujours de manière unique un utilisateur.
|
Le placement de ce fichier est aussi un peu particulier : modules/sfGuardAuth/lib/sfGuardValidatorCustomUser.class.php. En effet,
il vient remplacer des fonctionnalités du module sfGuardAuth, d'où son placement.
<?php
class sfGuardValidatorCustomUser extends sfGuardValidatorUser
{
protected function doClean($values)
{
$username = isset($values[$this->getOption('username_field')]) ? $values[$this->getOption('username_field')] : '';
$password = isset($values[$this->getOption('password_field')]) ? $values[$this->getOption('password_field')] : '';
if ($username && $password)
{
if($connected)
{
$query = Doctrine_Core::getTable('sfGuardUser')->createQuery('u')->where('u.id = ?', $id);
$user = $query->fetchOne();
if(! $user)
{
$user = new sfGuardUser();
$user->setId($id);
$user->setFirstName($prenom);
$user->setLastName($nom);
$user->setEmailAddress($email);
$user->setUsername($pseudo);
$user->setIsActive(true);
$user->setIsSuperAdmin(false);
$user->save();
}
return array_merge($values, array('user' => $user));
}
}
if ($this->getOption('throw_global_error'))
{
throw new sfValidatorError($this, 'invalid');
}
throw new sfValidatorErrorSchema($this, array($this->getOption('username_field') => new sfValidatorError($this, 'invalid')));
}
}
|
Maintenant, une question reste en suspens : que se passe-t-il si l'utilisateur décide de changer de pseudo ?
Ou de nom ? Ou de prénom ? Ou s'il est banni du système ?
V. Mettre à jour les informations des utilisateurs
C'est là que l'on remarque l'utilité de référencer les utilisateurs par leur ID, ce qui permet de s'affranchir de leur nom d'utilisateur.
Ils pourraient très bien ne pas en avoir que notre système fonctionnerait à merveille.
La méthode employée sera extrêmement simple : il suffit de vérifier que les informations n'ont pas changé. Le cas échéant, l'objet utilisateur
sera modifié et envoyé en base de données.
Très légère optimisation de logique : si un utilisateur vient d'être créé, ses paramètres ne peuvent pas avoir changé depuis que les informations ont
été récupérées. Si c'était le cas, il faudrait recharger les informations depuis le système d'authentification, ce qui serait extrêmement lourd et
foncièrement inutile.
Voici donc la portion de code ajoutée, elle se passe de commentaire supplémentaire :
if($user->getFirstName() != $prenom)
{
$user->setFirstName($prenom);
$user->save();
}
if($user->getLastName() != $nom)
{
$user->setLastName($nom);
$user->save();
}
if($user->getUsername() != $pseudo)
{
$user->setUsername($pseudo);
$user->save();
}
if($user->getEmailAddress() != $email)
{
$user->setEmailAddress($email);
$user->save();
}
if($user->getIsActive() != $active)
{
$user->setIsActive($active);
$user->save();
}
|
Notre validateur ressemble donc au final à ceci :
<?php
class sfGuardValidatorCustomUser extends sfGuardValidatorUser
{
protected function doClean($values)
{
$username = isset($values[$this->getOption('username_field')]) ? $values[$this->getOption('username_field')] : '';
$password = isset($values[$this->getOption('password_field')]) ? $values[$this->getOption('password_field')] : '';
if ($username && $password)
{
if($connected)
{
$query = Doctrine_Core::getTable('sfGuardUser')->createQuery('u')->where('u.id = ?', $id);
$user = $query->fetchOne();
if(! $user)
{
$user = new sfGuardUser();
$user->setId($id);
$user->setFirstName($prenom);
$user->setLastName($nom);
$user->setEmailAddress($email);
$user->setUsername($pseudo);
$user->setIsActive(true);
$user->setIsSuperAdmin(false);
$user->save();
}
else
{
if($user->getFirstName() != $prenom)
{
$user->setFirstName($prenom);
$user->save();
}
if($user->getLastName() != $nom)
{
$user->setLastName($nom);
$user->save();
}
if($user->getUsername() != $pseudo)
{
$user->setUsername($pseudo);
$user->save();
}
if($user->getEmailAddress() != $email)
{
$user->setEmailAddress($email);
$user->save();
}
if($user->getIsActive() != $active)
{
$user->setIsActive($active);
$user->save();
}
}
return array_merge($values, array('user' => $user));
}
}
if ($this->getOption('throw_global_error'))
{
throw new sfValidatorError($this, 'invalid');
}
throw new sfValidatorErrorSchema($this, array($this->getOption('username_field') => new sfValidatorError($this, 'invalid')));
}
}
|
 |
Évidemment, si vous ne récupérez pas certaines informations de votre identificateur externe, vous ne pourrez pas les inventer ;
dans ce cas, les passages du code associés pourront être simplement supprimés (et les valeurs par défaut définies par sfDoctrineGuardPlugin
seront utilisées).
|
VI. Gérer les droits
Il suffit d'enregistrer l'utilisateur dans un groupe ou l'autre lors de la connexion pour qu'il ait les droits de ce groupe. Vous pouvez aussi
ajouter droit par droit, en prenant l'utilisateur comme base ; étant donné que cela complexifie fortement la gestion des droits (il faut alors
regarder utilisateur par utilisateur, il ne suffit plus de changer les droits d'un groupe), je déconseille cette méthode. Elle ne sera d'ailleurs
pas montrée ici (mais vous pourrez adapter le code très facilement).
Tout d'abord, il vous faut un groupe auquel ajouter les utilisateurs. Dans une application backend, allez voir ce que propose le module sfGuardGroup :
http://localhost/project/sfGuardGroup/index ou http://localhost/project/guard/groups, en fonction de votre configuration
des routes. Là, retour direct à l'admin generator, bien connu des panels d'administration en tous genres. Créez-y simplement un groupe.
Pour qu'il ait un effet, il faut qu'il donne accès à des permissions, cela n'est pourtant pas nécessaire pour le reste du script.
Vous pouvez gérer les permissions dans http://localhost/project/sfGuardPermission/index ou
http://localhost/project/guard/permissions ; il faudra retourner dans le module précédent pour associer un groupe à une ou plusieurs
permissions.
Dans ce bout de code, nous considérerons que le module d'authentification externe renvoie une variable booléenne $r qui, vraie, indique
que l'utilisateur fait partie du groupe d'identifiant 42.
if($r)
{
$perm = new sfGuardUserGroup();
$perm->setUserId($id);
$perm->setGroupId(42);
$perm->save();
}
|
Et voilà ! Inutile d'en dire plus, on ne fait que créer l'enregistrement en base qui lie l'utilisateur d'identifiant $id
au groupe d'identifiant 42.
Maintenant, une question reste en suspens... Que se passe-t-il si l'utilisateur n'appartient plus à ce groupe lors de sa connexion ?
Il serait bon de l'en retirer. Quelle méthode allons-nous utiliser ?
if(! $r)
{
$q = Doctrine_Query::create()
->delete('sfGuardUserGroup u')
->where('u.user_id = ?', $id)
->andWhere('u.group_id');
$r->execute();
}
|
Pour plus de raffinement, votre méthode d'authentification externe pourrait vous renvoyer une liste de groupes auxquels l'utilisateur appartient.
Il suffit de récupérer les enregistrements de groupe correspondant et de créer les enregistrements. Considérons que sfDoctrineGuardPlugin
a à sa disposition l'entièreté des groupes disponibles et que nous récupérons un tableau $groups avec les noms des groupes
auxquels il faut associer l'utilisateur.
foreach($groups as $group)
{
$q = Doctrine_Query::create()
->select('g.id')
->from('sfGuardGroup g')
->where('g.name = ?', $group);
$grp = $r->fecthOne();
$perm = new sfGuardUserGroup();
$perm->setUser($user);
$perm->setGroup($grp);
$perm->save();
}
|
Remarquez les méthodes setUser() et setGroup() au lieu de setUserId() et setGroupId()
précédemment : ici, nous avons des objets sfGuardUser et sfGuardGroup, nous pouvons donc les passer en entier
en paramètres et non seulement leur identifiant. Ces deux méthodes sont strictement équivalentes.
VII. Conclusion
Ce plug-in de symfony, sfDoctrineGuardPlugin, est un plug-in-clé de symfony, il est très fréquent de le voir installé dans un projet tellement ses services
peuvent être indispensables. Il ne fait pas tout : le plug-in
sfForkedDoctrineApplyPlugin
vient répondre à certaines lacunes. En effet, sfDoctrineGuardPlugin ne fait que la gestion des utilisateurs et des droits, ainsi que de la connexion.
Pour le reste (inscription, confirmation par email des inscriptions, CAPTCHA lors de l'inscription, redirection après la connexion, etc.).
Remarquez aussi que plug-in a proposé avant sfDoctrineGuardPlugin un champ email dans la base des utilisateurs, manque qui n'a été comblé qu'avec la version 5.0.0.
Un tout grand merci à
Yogui pour m'avoir poussé à écrire cet article,
ainsi qu'à
Mahefasoa !
| (1) |
Notez qu'il est inutile de hasher son mot de passe et de le stocker comme si le plug-in allait l'utiliser,
cela empêcherait l'utilisateur de le modifier dans le système externe, ce qui n'est généralement pas désirable ; de plus, il faudrait personnaliser
cette méthode pour aussi en tenir compte... Un cas plus tordu est envisageable : pour une migration d'application, tous les utilisateurs doivent
se connecter à ce nouveau système, pour charger tous les mots de passe. Une fois tous les utilisateurs migrés, rebasculer vers le système
complet sfDoctrineGuardPlugin et effacer toute cette personnalisation. Ce n'est, je pense, un cas très fréquent.
|


Copyright © 2010 Thibaut Cuvelier. Aucune reproduction, même partielle, ne peut être faite
de ce site et de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 €
de dommages et intérêts.
Cette page est déposée.