Authentifier les utilisateurs
Presque aucune application web ne peut se passer d'un mécanisme de connexion et de déconnexion des utilisateurs ni de vérification de leurs permissions. Dans ce chapitre, nous parlerons de :
- la connexion et la déconnexion des utilisateurs
- les authenticators personnalisés
Dans les exemples, nous utiliserons un objet de la classe Nette\Security\User, qui représente l'utilisateur
courant et que vous obtenez en vous le faisant passer par injection de dépendances. Dans les presenters, il
suffit d'appeler $user = $this->getUser().
Authentification
L'authentification, c'est la connexion de l'utilisateur, autrement dit le processus au cours duquel son identité est
vérifiée. L'utilisateur s'identifie généralement par un nom d'utilisateur et un mot de passe. La vérification est assurée
par ce qu'on appelle l'Authenticator. Si la connexion échoue, une
Nette\Security\AuthenticationException est levée.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Le nom d\'utilisateur ou le mot de passe saisi est incorrect.');
}
Voici comment déconnecter l'utilisateur :
$user->logout();
Et pour savoir s'il est connecté :
echo $user->isLoggedIn() ? 'oui' : 'non';
Très simple, non ? Et Nette prend en charge pour vous tous les aspects de sécurité.
Dans les presenters, vous pouvez vérifier la connexion dans la méthode startup() et rediriger les utilisateurs
non connectés vers la page de connexion.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Expiration
La connexion de l'utilisateur expire en même temps que le stockage, qui
est généralement la session (voir le réglage de l'expiration
de la session). Vous pouvez cependant aussi fixer un intervalle plus court au bout duquel l'utilisateur est déconnecté. La
méthode setExpiration(), appelée avant login(), sert à cela. Passez-lui en argument une chaîne
contenant un temps relatif :
// la connexion expire après 30 minutes d'inactivité
$user->setExpiration('30 minutes');
// annule l'expiration définie
$user->setExpiration(null);
La méthode $user->getLogoutReason() révèle si l'utilisateur a été déconnecté parce que l'intervalle a
expiré. Elle renvoie soit la constante Nette\Security\User::LogoutInactivity (la limite de temps a expiré), soit
User::LogoutManual (la méthode logout() a été appelée).
Authenticator
C'est un objet qui vérifie les identifiants de connexion, typiquement le nom d'utilisateur et le mot de passe. Sa forme la plus élémentaire est la classe Nette\Security\SimpleAuthenticator, que l'on peut définir dans la configuration :
security:
users:
# nom d'utilisateur : mot de passe
johndoe: 'secret123'
kathy: 'evenmoresecretpassword'
Au lieu de mots de passe en clair, vous pouvez aussi indiquer leurs hachages ; voir la configuration.
Cette solution convient plutôt à des fins de test. Nous allons montrer comment créer un authenticator qui vérifie les identifiants de connexion dans une table de base de données.
Un authenticator est un objet implémentant l'interface Nette\Security\Authenticator avec la méthode
authenticate(). Sa tâche est soit de renvoyer une Identité, soit de lever une
Nette\Security\AuthenticationException. Il est aussi possible d'indiquer un code d'erreur pour distinguer plus
finement la situation : Authenticator::IdentityNotFound ou Authenticator::InvalidCredential.
use Nette;
use Nette\Security\SimpleIdentity;
class MyAuthenticator implements Nette\Security\Authenticator
{
public function __construct(
private Nette\Database\Explorer $database,
private Nette\Security\Passwords $passwords,
) {
}
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->database->table('users')
->where('username', $username)
->fetch();
if (!$row) {
throw new Nette\Security\AuthenticationException('Utilisateur non trouvé.');
}
if (!$this->passwords->verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Mot de passe invalide.');
}
return new SimpleIdentity(
$row->id,
$row->role, // ou un tableau de rôles
['name' => $row->username],
);
}
}
La classe MyAuthenticator communique avec la base de données via Nette Database Explorer et travaille avec la table users, où
la colonne username contient le nom de connexion de l'utilisateur et la colonne password le hachage du mot de passe. Après avoir vérifié le nom et le mot de passe,
elle renvoie l'identité contenant l'ID de l'utilisateur, son rôle (la colonne role de la table), dont nous
parlerons davantage plus loin, et un tableau de données
supplémentaires (dans notre cas, le nom d'utilisateur).
Nous ajouterons l'authenticator à la configuration comme service du conteneur DI :
services:
- MyAuthenticator
Événements $onLoggedIn, $onLoggedOut
L'objet Nette\Security\User possède les événements $onLoggedIn et $onLoggedOut,
vous pouvez donc ajouter des callbacks déclenchés respectivement après une connexion réussie ou après la déconnexion de
l'utilisateur.
$user->onLoggedIn[] = function () {
// l'utilisateur vient de se connecter
};
Identité
Une identité est un ensemble d'informations sur un utilisateur, renvoyé par l'authenticator, stocké ensuite dans la session
et récupérable avec $user->getIdentity(). Cela nous permet d'obtenir l'ID, les rôles et les autres données de
l'utilisateur, exactement comme nous les avons passés dans l'authenticator :
$user->getIdentity()->getId();
// le raccourci $user->getId() fonctionne aussi
$user->getIdentity()->getRoles();
// les données de l'utilisateur sont accessibles comme propriétés
// le nom d'utilisateur que nous avons passé dans MyAuthenticator
$user->getIdentity()->name;
Le point important est que, lors de la déconnexion par $user->logout(), l'identité n'est pas
supprimée et reste disponible. Ainsi, même si un utilisateur a une identité, il n'est pas forcément connecté. Si nous
voulons supprimer explicitement l'identité, nous déconnectons l'utilisateur en appelant logout(true).
Grâce à cela, vous pouvez toujours supposer quel utilisateur est devant l'ordinateur et, par exemple, afficher des offres personnalisées dans une boutique en ligne, mais vous ne pouvez afficher ses informations personnelles qu'après sa connexion.
Outre l'effacement de l'identité au cas par cas avec logout(true), vous pouvez désactiver
entièrement sa conservation à l'aide de la propriété $persistIdentity. Fixée à false, l'identité
est jetée à chaque déconnexion et à l'expiration, si bien que getIdentity() renvoie alors null. La
conservation de l'identité dépend aussi du stockage : le stockage par cookie ne peut pas la conserver après la déconnexion,
car il supprime toujours le cookie.
Une identité est un objet implémentant l'interface Nette\Security\IIdentity. L'implémentation par défaut est Nette\Security\SimpleIdentity. Et, comme nous l'avons dit, elle est conservée en session : si nous changeons par exemple le rôle de l'un des utilisateurs connectés, les anciennes données resteront dans son identité jusqu'à sa prochaine connexion.
Stockage de l'utilisateur connecté
Les deux informations de base sur l'utilisateur, à savoir s'il est connecté et son Identité,
sont généralement transmises dans la session. Ce qui peut être changé. Un objet implémentant l'interface
Nette\Security\UserStorage est chargé de stocker ces informations. Deux implémentations standards sont disponibles
: Nette\Bridges\SecurityHttp\SessionStorage, qui transmet les données en session, et CookieStorage, qui
les transmet dans un cookie. Vous pouvez choisir le stockage et le configurer très commodément dans la configuration security › authentication.
Vous pouvez en outre influencer la façon exacte dont se déroulent l'enregistrement (sleep) et la restauration
(wakeup) de l'identité. Il suffit que l'authenticator implémente l'interface Nette\Security\IdentityHandler.
La méthode sleepIdentity() est appelée avant l'écriture de l'identité dans le stockage, et
wakeupIdentity() après sa lecture. Ces méthodes peuvent modifier le contenu de l'identité, ou la remplacer par un
nouvel objet qu'elles renvoient. La méthode wakeupIdentity() peut même renvoyer null, ce qui
déconnecte l'utilisateur. L'interface déclare aussi la méthode getGuestIdentity(), voir Identité invité.
Montrons en exemple la solution à une question fréquente : comment mettre à jour les rôles de l'identité juste après son
chargement depuis la session. Dans la méthode wakeupIdentity(), nous passons dans l'identité les rôles actuels,
par exemple issus d'une base de données :
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// ici, vous pouvez modifier l'identité avant son écriture dans le stockage après la connexion,
// mais nous n'en avons pas besoin pour l'instant
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// met à jour les rôles de l'identité
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
public function getGuestIdentity(): ?IIdentity
{
// aucune identité invité n'est utilisée ici
return null;
}
Revenons maintenant au stockage par cookies. Il vous permet de créer un site où les utilisateurs peuvent se connecter sans
avoir besoin de sessions. Il n'a donc pas besoin d'écrire sur le disque. C'est ainsi que fonctionne le site que vous lisez
actuellement, forum compris. Dans ce cas, l'implémentation d'IdentityHandler est indispensable. Nous ne stockerons
dans le cookie qu'un jeton aléatoire représentant l'utilisateur connecté.
Réglez d'abord le stockage voulu dans la configuration à l'aide de
security › authentication › storage: cookie.
Dans la base de données, créez la colonne authtoken, où chaque utilisateur aura une chaîne totalement aléatoire, unique et impossible à deviner et suffisamment longue (au
moins 13 caractères). Le CookieStorage ne transmet dans le cookie que la valeur $identity->getId()
: dans sleepIdentity(), nous remplaçons donc l'identité d'origine par une identité proxy contenant
l'authtoken dans l'ID. Inversement, dans la méthode wakeupIdentity(), nous lisons toute l'identité
depuis la base d'après l'authtoken :
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->db->fetch('SELECT * FROM user WHERE username = ?', $username);
// vérifie le mot de passe
// ...
// renvoie l'identité avec toutes les données de la base
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// renvoie une identité proxy dont l'ID contient l'authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// remplace l'identité proxy par l'identité complète, comme dans authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
public function getGuestIdentity(): ?IIdentity
{
// aucune identité invité n'est utilisée ici
return null;
}
}
Identité invité
Il est parfois pratique que les visiteurs non connectés aient eux aussi une identité, par exemple pour leur attribuer un jeu
de rôles par défaut ou certaines données. Si l'authenticator implémente IdentityHandler, il peut en fournir une
par la méthode getGuestIdentity(), utilisée chaque fois que personne n'est connecté. getIdentity(),
getId() et getRoles() se rabattent alors sur elle, si bien que les invités peuvent avoir leurs propres
rôles au lieu du simple rôle guest. Renvoyez null si vous ne voulez pas d'identité invité.
public function getGuestIdentity(): ?IIdentity
{
return new SimpleIdentity('guest', ['guest'], ['name' => 'Invité']);
}
L'identité invité n'est jamais enregistrée dans le stockage, et la connexion la remplace toujours.
Plusieurs connexions indépendantes
Il est possible d'avoir plusieurs utilisateurs connectés indépendamment au sein d'un même site et d'une même session. Si nous voulons par exemple une authentification distincte pour l'administration et pour la partie publique du site, il suffit de définir un espace de noms unique pour chacune :
$user->getStorage()->setNamespace('backend');
Il est important de penser à définir l'espace de noms partout dans la partie concernée. Si nous utilisons des presenters, nous le définissons dans l'ancêtre commun de cette partie, généralement BasePresenter. Nous le faisons en étendant la méthode checkRequirements() :
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Si vous changez d'espace de noms au cours d'une même requête (après que l'état d'authentification a déjà été lu),
l'objet User conserve encore l'état mis en cache depuis l'espace de noms précédent. Dans ce cas, appelez
refreshStorage() pour jeter le cache et forcer un rechargement depuis le nouvel espace de noms :
$user->getStorage()->setNamespace('admin');
$user->refreshStorage(); // recharge l'état depuis le nouvel espace de noms
Plusieurs authenticators
Diviser une application en parties à connexion indépendante exige généralement aussi des authenticators différents. Si
nous enregistrions cependant deux classes implémentant Authenticator dans la configuration des services, Nette ne saurait pas
laquelle affecter automatiquement à l'objet Nette\Security\User et afficherait une erreur. Nous devons donc
restreindre l'autowiring des authenticators pour qu'il ne
fonctionne que lorsque quelqu'un demande une classe précise, par exemple FrontAuthenticator. On y parvient en
choisissant autowired: self :
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Nous définissons l'authenticator de l'objet User avant l'appel de la méthode login(), donc généralement dans le code du formulaire qui connecte l'utilisateur :
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};