Connexion des utilisateurs (Authentification)

Presque aucune application web ne peut se passer d'un mécanisme de connexion des utilisateurs et de vérification des autorisations des utilisateurs. Dans ce chapitre, nous parlerons de :

  • connexion et déconnexion des utilisateurs
  • authentificateurs personnalisés

Installation et prérequis

Dans les exemples, nous utiliserons l'objet de la classe Nette\Security\User, qui représente l'utilisateur actuel et auquel vous pouvez accéder en le faisant passer via l'injection de dépendances. Dans les presenters, il suffit d'appeler $user = $this->getUser().

Authentification

L'authentification désigne la connexion des utilisateurs, c'est-à-dire le processus par lequel on vérifie si l'utilisateur est bien celui qu'il prétend être. Habituellement, il se prouve par un nom d'utilisateur et un mot de passe. La vérification est effectuée par un soi-disant autentikátor. 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 est incorrect');
}

De cette manière, vous déconnectez l'utilisateur :

$user->logout();

Et pour savoir s'il est connecté :

echo $user->isLoggedIn() ? 'oui' : 'non';

Très simple, n'est-ce pas ? Et Nette s'occupe de tous les aspects de sécurité pour vous.

Dans les presenters, vous pouvez vérifier la connexion dans la méthode startup() et rediriger l'utilisateur non connecté 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 l'expiration du stockage, qui est généralement la session (voir les paramètres d'expiration de session). Cependant, il est possible de définir un intervalle de temps plus court, après lequel l'utilisateur sera déconnecté. Pour cela, on utilise la méthode setExpiration(), qui est appelée avant login(). Comme paramètre, indiquez une chaîne avec un temps relatif :

// la connexion expirera après 30 minutes d'inactivité
$user->setExpiration('30 minutes');

// annulation de l'expiration définie
$user->setExpiration(null);

Si l'utilisateur a été déconnecté en raison de l'expiration de l'intervalle de temps, la méthode $user->getLogoutReason() l'indiquera, renvoyant soit la constante Nette\Security\UserStorage::LogoutInactivity (le délai a expiré) soit UserStorage::LogoutManual (déconnecté par la méthode logout()).

Authentificateur

Il s'agit d'un objet qui vérifie les informations d'identification, c'est-à-dire généralement le nom et le mot de passe. Une forme triviale est la classe Nette\Security\SimpleAuthenticator, que nous pouvons définir dans la configuration :

security:
	users:
		# nom: mot de passe
		frantisek: motdepassesecret
		katka: encoremotdepassesecret

Cette solution convient plutôt à des fins de test. Nous allons montrer comment créer un authentificateur qui vérifiera les informations d'identification par rapport à une table de base de données.

L'authentificateur est un objet implémentant l'interface Nette\Security\Authenticator avec la méthode authenticate(). Sa tâche est soit de retourner une soi-disant identité, soit de lever une exception Nette\Security\AuthenticationException. Il serait possible d'y ajouter un code d'erreur pour distinguer plus finement la situation : Authenticator::IdentityNotFound et 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 plusieurs 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 contient l'empreinte du mot de passe. Après vérification du nom et du mot de passe, elle renvoie l'identité, qui contient l'ID de l'utilisateur, son rôle (colonne role dans la table), dont nous parlerons plus en détail plus tard, et un tableau avec d'autres données (dans notre cas, le nom d'utilisateur).

Nous ajoutons encore l'authentificateur à la configuration en tant que service du conteneur DI :

services:
	- MyAuthenticator

Événements $onLoggedIn, $onLoggedOut

L'objet Nette\Security\User a des événements $onLoggedIn et $onLoggedOut, vous pouvez donc ajouter des rappels qui seront appelés après une connexion réussie ou après la déconnexion de l'utilisateur.

$user->onLoggedIn[] = function () {
	// l'utilisateur vient d'être connecté
};

Identité

L'identité représente un ensemble d'informations sur l'utilisateur, renvoyé par l'authentificateur et ensuite stocké dans la session, que nous obtenons à l'aide de $user->getIdentity(). Nous pouvons ainsi obtenir l'id, les rôles et d'autres données utilisateur, telles que nous les avons transmises dans l'authentificateur :

$user->getIdentity()->getId();
// le raccourci $user->getId() fonctionne également ;

$user->getIdentity()->getRoles();

// les données utilisateur sont accessibles comme des propriétés
// le nom que nous avons transmis dans MyAuthenticator
$user->getIdentity()->name;

Ce qui est important, c'est qu'après la déconnexion via $user->logout(), l'identité n'est pas supprimée et reste disponible. Ainsi, même si l'utilisateur a une identité, il n'est pas nécessairement connecté. Si nous voulions supprimer explicitement l'identité, nous déconnecterions l'utilisateur en appelant logout(true).

Grâce à cela, vous pouvez continuer à supposer quel utilisateur est devant l'ordinateur et, par exemple, lui afficher des offres personnalisées dans une boutique en ligne, mais vous ne pouvez afficher ses données personnelles qu'après connexion.

L'identité est un objet implémentant l'interface Nette\Security\IIdentity, l'implémentation par défaut est Nette\Security\SimpleIdentity. Et comme mentionné, elle est maintenue dans la session, donc si, par exemple, nous changeons 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é

Deux informations de base sur l'utilisateur, à savoir s'il est connecté et son identita, sont généralement transmises dans la session. Ce qui peut être modifié. Le stockage de ces informations est géré par un objet implémentant l'interface Nette\Security\UserStorage. Deux implémentations standard sont disponibles, la première transmet les données dans la session et la seconde dans un cookie. Il s'agit des classes Nette\Bridges\SecurityHttp\SessionStorage et CookieStorage. Vous pouvez choisir le stockage et le configurer très facilement dans la configuration security › authentication.

De plus, vous pouvez influencer la manière exacte dont le stockage de l'identité (sleep) et la restauration (wakeup) se dérouleront. Il suffit que l'authentificateur implémente l'interface Nette\Security\IdentityHandler. Celle-ci a deux méthodes : sleepIdentity() est appelée avant l'écriture de l'identité dans le stockage et wakeupIdentity() après sa lecture. Les méthodes peuvent modifier le contenu de l'identité, ou la remplacer par un nouvel objet qu'elles retournent. La méthode wakeupIdentity() peut même retourner null, ce qui déconnecte l'utilisateur.

Comme exemple, montrons la solution à la question fréquente de savoir comment mettre à jour les rôles dans l'identité immédiatement après le chargement depuis la session. Dans la méthode wakeupIdentity(), nous transmettons à l'identité les rôles actuels, par exemple depuis la base de données :

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// ici on peut modifier l'identité avant l'écriture dans le stockage après la connexion,
		// mais nous n'en avons pas besoin maintenant
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// mise à jour des rôles dans l'identité
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

Et maintenant, revenons au stockage basé sur les cookies. Il vous permet de créer un site web où les utilisateurs peuvent se connecter sans avoir besoin de sessions. C'est-à-dire qu'il n'a pas besoin d'écrire sur le disque. D'ailleurs, le site web que vous lisez actuellement fonctionne ainsi, y compris le forum. Dans ce cas, l'implémentation de IdentityHandler est une nécessité. En effet, nous ne stockerons dans le cookie qu'un jeton aléatoire représentant l'utilisateur connecté.

Tout d'abord, dans la configuration, nous définissons le stockage souhaité à l'aide de security › authentication › storage: cookie.

Dans la base de données, nous créons une colonne authtoken, dans laquelle chaque utilisateur aura une chaîne complètement aléatoire, unique et impossible à deviner d'une longueur suffisante (au moins 13 caractères). Le stockage CookieStorage ne transmet dans le cookie que la valeur $identity->getId(), donc dans sleepIdentity(), nous remplaçons l'identité originale par une identité substitutive avec authtoken dans l'ID, et inversement, dans la méthode wakeupIdentity(), nous lisons l'identité complète depuis la base de données en fonction de 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);
		// nous vérifions le mot de passe
		...
		// nous retournons l'identité avec toutes les données de la base de données
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// nous retournons une identité substitutive, où l'ID sera l'authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// nous remplaçons l'identité substitutive 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;
	}
}

Plusieurs connexions indépendantes

Il est possible d'avoir plusieurs utilisateurs connectés indépendamment au sein d'un même site web et d'une même session. Si, par exemple, nous voulons avoir une authentification distincte pour l'administration et la partie publique sur le site web, il suffit de définir un nom propre pour chacune d'elles :

$user->getStorage()->setNamespace('backend');

Il est important de se rappeler de toujours définir l'espace de noms à tous les endroits appartenant à la partie concernée. Si nous utilisons des presenters, nous définissons l'espace de noms dans l'ancêtre commun pour la partie donnée – 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);
}

Plusieurs authentificateurs

La division de l'application en parties avec connexion indépendante nécessite généralement également différents authentificateurs. Cependant, si nous enregistrions deux classes implémentant Authenticator dans la configuration des services, Nette ne saurait pas laquelle attribuer automatiquement à l'objet Nette\Security\User, et afficherait une erreur. Par conséquent, nous devons limiter l'autowiring pour les authentificateurs afin qu'il ne fonctionne que si quelqu'un demande une classe spécifique, par exemple FrontAuthenticator, ce que nous réalisons 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'authentificateur de l'objet User avant d'appeler la méthode login(), donc généralement dans le code du formulaire qui le connecte :

$form->onSuccess[] = function (Form $form, \stdClass $data) {
	$user = $this->getUser();
	$user->setAuthenticator($this->authenticator);
	$user->login($data->username, $data->password);
	// ...
};
version: 4.0