Implémenter un UserProvider

Suite au premier article que j’avais rédigé pour mon entreprise qui mettait en avant l’utilisation de la sécurité (firewalls, authentification…), et d’un second article que j’ai rédigé sur ce propre article qui met en avant les méthodes permettant d’ajouter les fonctionnalités essentielles comme créer un compte, valider son compte, changer son mot de passe ou encore générer un nouveau mot de passe, j’ai décidé d’écrire un troisième et dernier article sur ce sujet, pour vous décrire comment implémenter son propre provider d’authentification.

À quoi sert un provider pour l’authentification ?

Le provider d’authentification est l’élément qui permet à un utilisateur qui souhaiterai s’authentifier d’être rattaché à un utilisateur d’une base de donnée ou dans renvoyé une erreur si les informations ne sont pas correctes. De base, Symfony2 propose un provider implémenter ce qui ne vous oblige pas à un implémenter un.

Pourquoi implémenter son propre provider

Implémenter son propre provider peut avoir plusieurs intérêts.

  • Vous pourriez vouloir que votre utilisateur s’authentifie de plusieurs manières. Saisir son login, son username, son email ou tout autre information unique pourrait être utile.
  • Nous pourrions aussi vouloir gérer les authentifications. Par exemple, un compte pourrait tout à fait exister mais celui-ci pourrait ne pas être activé, ou aurait pu être désactivé. Tous ces éléments peuvent rapidement être nécessaire pour le développer.
  • Dans le cas d’une application sensible, on pourrait également vouloir envoyer un mail à la personne qui se connecte au compte, dans le but par exemple de détecter une activité suspecte.
  • Toutes autres raisons…

Comment implémenter son provider

Sensio a mis en place pour son produit une interface rapide à implémenter. Il s’agit de UserProviderInterface.

Vous devrez implémenter trois méthodes :

  • loadUserByUsername
  • refreshUser
  • supportsClass
    loadUserByUsername permet de charger l’utilisateur correspondant aux informations données. C’est cette méthode qui est la plus importante pour nous. De là, vous pouvez charger l’utilisateur selon les règles que vous souhaitez pour votre application mais vous également lever moultes exceptions pour restreindre l’accès. Voici la liste complète des exceptions que vous pourrez levé.

  • BadCredentialsException est l’exception que vous risquez d’utiliser le plus. Elle doit vous permettre de déclarer à la personne qui tente de se connecter que les informations saisies ne correspondent à aucune entité en base de donnée.

  • DisabledException sera levé si un compte qui aurait été désactivé par un administrateur tente d’être contacté. Cela pourrait être nécessaire si le compte requiert une validation par mail du compte par exemple.
  • LockedException ressemble fortement à l’exception précédente DisabledException et sera créée dans le cas où le compte aurait été bloqué définitivement.
    Toutes les autres exceptions peuvent avoir un rôle dans votre application. Toutes les descriptions vous permettrons de savoir laquelle peut vous servir dans telle ou telle cas.

Code d’un UserProvider

loadUserByUsername implémentation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
public function loadUserByUsername($username)
{
    $criteria = new Criteria();
    $criteria->where(new Comparison("username", Comparison::EQ, $username));
    $criteria->orWhere(new Comparison("email", Comparison::EQ, $username));
    $criteria->setMaxResults(1);
    $userData = $this->em->getRepository("NamespaceMyBundle:User")->matching($criteria)->first();
    if ($userData != null) {
        switch ($userData->getActive()) {
            case User::DISABLED:
            throw new DisabledException("Your account is disabled. Please contact the administrator.");
                break;
            case User::WAIT_VALIDATION:
                throw new LockedException("Your account is locked. Check and valid your email account.");
                break;
            case User::ACTIVE:
                return $userData;
                break;
        }
    }
    throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}

Dans cette implémentation, nous cherchons un utilisateur qui pourrait avoir comme username ou comme mail l’objet $username qui a été passé en paramètre à la méthode.

Si aucune entité ne correspond à ma recherche, une exception UsernameNotFoundException sera levé.

Si un utilisateur a été trouvé, nous allons regardé si son compte est actif. Dans un autre cas, on lèvera une exception pour informer l’utilisateur du problème qui l’empêche de se connecter.

Si on souhaite connecter un utilisateur, il nous suffit juste de renvoyer l’objet (à condition qu’il implémente bien l’interface UserInterface).

refreshUser implémentation

Cette méthode est rapide à implementer dans mon cas et je ne vois pas de raison que son comportement change mais libre à vous de modifier cette méthode pour qu’elle réponde à vos besoins.

Elle doit regarder si l’objet correspond bien au type d’instance qu’on gère. Dans ce cas, on appelle loadUserByUsername.

supportsClass implémentation

Cette méthode renvoie un booléen, si le type de l’objet donnée est supporté.

Déclarer et utiliser son propre provider

Avant cela, si vous aviez suivis mon premier article sur comment utiliser l’authentification nativement dans Symfony2, vous devriez avois déclarer dans votre ficher app/config/security.ml, un provider comme ceci.

1
2
3
4
security:
    providers:
        main:
            entity: { class: Namespace\MyBundle\Entity\User, property: username }

Si vous voulez utilisez un provider que vous auriez implémenter voici les deux déclarations à réaliser.

Déclarer le provider comme un service

On commence par déclarer le provider comme un service. Dans mon cas, j’injecte mon manager doctrine pour réaliser mes recherches grâce à l’ORM. Vous pourriez vouloir injecter d’autres services. Je vous conseille donc de suivre ces petits conseilles d’injection des dépendances.

1
2
3
4
services:
    security_userprovider:
        class: Namespace\MyBundle\Security\User\UserProvider
        arguments: [ @doctrine.orm.default_entity_manager ]

Déclarer le provider que l’on souhaite utiliser

Désormais que votre provider est déclarer, nous allons demander au firewall qui s’occupe de la gestion des utilisateurs pour notre cas, de ce servir du service ayant pour id _securityuserprovider.

1
2
3
4
security:
    providers:
        main:
            id: security_userprovider

Conclusion

L’implémentation d’un provider d’authentification est très simple a réalisé et peu nous permettre de réaliser un code propre. L’intégralité de la documentation Symfony est disponible ici.

Vous pouvez retrouver le code utiliser dans le Gist qui suit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
namespace Namespace\MyBundle\Security\User;
use Doctrine\ORM\EntityManager;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class UserProvider implements UserProviderInterface
{
private $em;
public function __construct(EntityManager $em) {
$this->em = $em;
}
public function loadUserByUsername($username)
{
$criteria = new Criteria();
$criteria->where(new Comparison("username", Comparison::EQ, $username));
$criteria->orWhere(new Comparison("email", Comparison::EQ, $username));
$criteria->setMaxResults(1);
$userData = $this->em->getRepository("NamespaceMyBundle:User")->matching($criteria)->first();
if ($userData != null) {
switch ($userData->getActive()) {
case User::DISABLED:
throw new DisabledException("Your account is disabled. Please contact the administrator.");
break;
case User::_WAIT_VALIDATION:
throw new LockedException("Your account is locked. Check and valid your email account.");
break;
case User::ACTIVE:
return $userData;
break;
}
}
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'Namespace\MyBundle\Entity\User';
}
}