Authentification des utilisateurs

Les applications Web peu nombreuses n'ont pas besoin d'un mécanisme de connexion des utilisateurs ou de vérification de leurs privilèges. Dans ce chapitre, nous parlerons de :

  • la connexion et la déconnexion des utilisateurs
  • les authentificateurs et autorisateurs personnalisés

Installation et exigences

Dans les exemples, nous utiliserons un objet de classe Nette\Security\User, qui représente l'utilisateur actuel et que vous obtenez en le passant à l'aide de l'injection de dépendances. Dans les présentateurs, il suffit d'appeler $user = $this->getUser().

Authentification

L'authentification signifie la connexion de l'utilisateur, c'est-à-dire le processus au cours duquel l'identité d'un utilisateur est vérifiée. L'utilisateur s'identifie généralement à l'aide d'un nom d'utilisateur et d'un mot de passe. La vérification est effectuée par l'authentificateur. Si la connexion échoue, le message Nette\Security\AuthenticationException est lancé.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('The username or password you entered is incorrect.');
}

Voici comment déconnecter l'utilisateur :

$user->logout();

Et vérifier si l'utilisateur est connecté :

echo $user->isLoggedIn() ? 'yes' : 'no';

Simple, non ? Et tous les aspects de la sécurité sont gérés par Nette pour vous.

Dans le présentateur, vous pouvez vérifier la connexion dans la méthode startup() et rediriger un 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 référentiel, qui est généralement une session (voir le paramètre d'expiration de la session ). Cependant, vous pouvez également définir un intervalle de temps plus court après lequel l'utilisateur est déconnecté. La méthode setExpiration(), qui est appelée avant login(), est utilisée à cette fin. Fournissez une chaîne de caractères avec une heure relative comme paramètre :

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

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

La méthode $user->getLogoutReason() indique si l'utilisateur a été déconnecté parce que l'intervalle de temps a expiré. Elle renvoie soit la constante Nette\Security\UserStorage::LogoutInactivity si le temps a expiré, soit UserStorage::LogoutManual si la méthode logout() a été appelée.

Authentificateur

C'est un objet qui vérifie les données de connexion, c'est-à-dire généralement le nom et le mot de passe. L'implémentation triviale est la classe Nette\Security\SimpleAuthenticator, qui peut être définie dans la configuration:

security:
	users:
		# name: password
		johndoe: secret123
		kathy: evenmoresecretpassword

Cette solution est plus adaptée à des fins de test. Nous allons vous montrer comment créer un authentificateur qui vérifiera les informations d'identification par rapport à une table de base de données.

Un authentificateur est un objet qui implémente l'interface Nette\Security\Authenticator avec la méthode authenticate(). Sa tâche consiste soit à renvoyer la dite identité, soit à lever une exception Nette\Security\AuthenticationException. Il serait également possible de fournir un code d'erreur à grain fin 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('User not found.');
		}

		if (!$this->passwords->verify($password, $row->password)) {
			throw new Nette\Security\AuthenticationException('Mot de passe non valide.');
		}

		return new SimpleIdentity(
			$row->id,
			$row->role, // ou tableau de rôles
			['name' => $row->username],
		);
	}
}

La classe MyAuthenticator communique avec la base de données par le biais de 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 le hachage. Après avoir vérifié le nom et le mot de passe, elle renvoie l'identité avec l'ID de l'utilisateur, le rôle (colonne role dans la table), que nous mentionnerons plus tard, et un tableau avec des données supplémentaires (dans notre cas, le nom d'utilisateur).

Nous ajouterons l'authentificateur à la configuration comme un service du conteneur DI :

services:
	- MyAuthenticator

Événements $onLoggedIn, $onLoggedOut

L'objet Nette\Security\User possède des événements $onLoggedIn et $onLoggedOut, ce qui vous permet d'ajouter des rappels qui sont déclenchés 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 qui est renvoyé par l'authentificateur et qui est ensuite stocké dans une session et récupéré en utilisant $user->getIdentity(). Nous pouvons donc récupérer l'identifiant, les rôles et les autres données de l'utilisateur comme nous les avons passés dans l'authentificateur :

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

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

// les données de l'utilisateur peuvent être accessibles en tant que propriétés
// le nom que nous avons transmis dans MyAuthenticator
$user->getIdentity()->name;

Il est important de noter que lorsque l'utilisateur se déconnecte en utilisant $user->logout(), l'identité n'est pas supprimée et est toujours disponible. Donc, si l'identité existe, elle ne garantit pas en soi que l'utilisateur est également connecté. Si nous voulons explicitement supprimer l'identité, nous déconnectons l'utilisateur par logout(true).

Grâce à cela, vous pouvez toujours supposer quel utilisateur se trouve sur l'ordinateur et, par exemple, afficher des offres personnalisées dans l'e-shop, mais vous ne pouvez afficher ses données personnelles qu'après vous être connecté.

L'identité est un objet qui implémente l'interface Nette\Security\IIdentity, l'implémentation par défaut est Nette\Security\SimpleIdentity. Et comme mentionné, l'identité est stockée dans la session, donc si, par exemple, nous changeons le rôle de certains des utilisateurs connectés, les anciennes données seront conservées dans l'identité jusqu'à ce qu'il se connecte à nouveau.

Stockage pour l'utilisateur connecté

Les deux informations de base concernant l'utilisateur, à savoir s'il est connecté et son identité, sont généralement transportées dans la session. Celles-ci peuvent être modifiées. Pour stocker ces informations, il faut un objet implémentant l'interface Nette\Security\UserStorage. Il existe deux implémentations standard, la première transmet les données dans une session et la seconde dans un cookie. Ce sont les classes Nette\Bridges\SecurityHttp\SessionStorage et CookieStorage. Vous pouvez choisir le stockage et le configurer de manière très pratique dans la configuration sécurité › authentification.

Vous pouvez également contrôler exactement comment la sauvegarde (sleep) et la restauration (wakeup) de l'identité se dérouleront. Il suffit que l'authentificateur implémente l'interface Nette\Security\IdentityHandler. Celle-ci possède deux méthodes : sleepIdentity() est appelée avant que l'identité ne soit écrite dans le stockage, et wakeupIdentity() est appelée après que l'identité ait été lue. Ces méthodes peuvent modifier le contenu de l'identité, ou la remplacer par un nouvel objet qui renvoie. La méthode wakeupIdentity() peut même renvoyer null, qui déconnecte l'utilisateur.

À titre d'exemple, nous allons montrer une solution à une question courante sur la façon de mettre à jour les rôles d'une identité juste après la restauration d'une session. Dans la méthode wakeupIdentity(), nous transmettons les rôles actuels à l'identité, 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, vous pouvez modifier l'identité avant de la stocker 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, nous 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 d'utiliser des sessions. Il n'est donc pas nécessaire d'écrire sur le disque. Après tout, c'est ainsi que fonctionne le site Web que vous êtes en train de lire, y compris le forum. Dans ce cas, l'implémentation de IdentityHandler est une nécessité. Nous stockerons seulement un jeton aléatoire représentant l'utilisateur connecté dans le cookie.

Nous commençons donc par définir le stockage souhaité dans la configuration à l'aide de security › authentication › storage: cookie.

Nous ajouterons une colonne authtoken dans la base de données, dans laquelle chaque utilisateur aura une chaîne de caractères complètement aléatoire, unique et impossible à deviner, d'une longueur suffisante (au moins 13 caractères). Le référentiel CookieStorage ne stocke que la valeur $identity->getId() dans le cookie, donc dans sleepIdentity() nous remplaçons l'identité originale par un proxy avec authtoken dans l'ID, au contraire dans la méthode wakeupIdentity() nous restaurons l'identité entière depuis la base de données selon 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érifier 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 renvoyons une identité proxy, où l'ID est l'authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// remplace l'identité du proxy par une 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;
	}
}

Authentifications indépendantes multiples

Il est possible d'avoir plusieurs utilisateurs connectés indépendants sur un même site et une seule session à la fois. Par exemple, si nous voulons avoir une authentification séparée pour le frontend et le backend, il suffit de définir un espace de nom de session unique pour chacun d'eux :

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

Il est nécessaire de garder à l'esprit que cela doit être défini à tous les endroits appartenant au même segment. Lorsque nous utilisons des présentateurs, nous définissons l'espace de nom dans l'ancêtre commun, généralement le BasePresenter. Pour ce faire, nous allons étendre la méthode checkRequirements():

public function checkRequirements($element): void
{
	$this->getUser()->getStorage()->setNamespace('backend');
	parent::checkRequirements($element);
}

Authentificateurs multiples

Diviser une application en segments avec une authentification indépendante nécessite généralement des authentificateurs différents. Cependant, l'enregistrement de deux classes qui implémentent l'Authenticator dans les services de configuration déclencherait une erreur car Nette ne saurait pas laquelle d'entre elles doit être autowired à l'objet Nette\Security\User. C'est pourquoi nous devons limiter l'autowiring pour eux avec autowired: self afin qu'il ne soit activé que lorsque leur classe est spécifiquement demandée :

services:
	-
		create: FrontAuthenticator
		autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FrontAuthenticator $authenticator,
	) {
	}
}

Nous devons seulement définir notre authentificateur sur l'objet User avant d'appeler la méthode login(), ce qui signifie généralement dans le callback du formulaire de connexion :

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