Inicio de sesión de usuarios (Autenticación)

Casi ninguna aplicación web puede prescindir de un mecanismo para iniciar sesión de usuarios y verificar los permisos de usuario. En este capítulo hablaremos sobre:

  • inicio y cierre de sesión de usuarios
  • autenticadores personalizados

Instalación y requisitos

En los ejemplos utilizaremos un objeto de la clase Nette\Security\User, que representa al usuario actual y al que puedes acceder solicitándolo mediante inyección de dependencias. En los presentadores, basta con llamar a $user = $this->getUser().

Autenticación

Por autenticación se entiende el inicio de sesión de usuarios, es decir, el proceso mediante el cual se verifica si un usuario es realmente quien dice ser. Normalmente se demuestra mediante un nombre de usuario y una contraseña. La verificación la realiza el llamado autentikátor. Si el inicio de sesión falla, se lanza Nette\Security\AuthenticationException.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('El nombre de usuario o la contraseña son incorrectos');
}

De esta manera cierras la sesión del usuario:

$user->logout();

Y para saber si ha iniciado sesión:

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

Muy simple, ¿verdad? Y Nette se encarga de todos los aspectos de seguridad por ti.

En los presentadores, puedes verificar el inicio de sesión en el método startup() y redirigir al usuario no autenticado a la página de inicio de sesión.

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isLoggedIn()) {
		$this->redirect('Sign:in');
	}
}

Expiración

El inicio de sesión del usuario expira junto con la expiración del almacenamiento, que suele ser la sesión (ver configuración de expiración de sesión). Sin embargo, también se puede establecer un intervalo de tiempo más corto, después del cual el usuario será desconectado. Para esto sirve el método setExpiration(), que se llama antes de login(). Como parámetro, indica una cadena con tiempo relativo:

// el inicio de sesión expira después de 30 minutos de inactividad
$user->setExpiration('30 minutes');

// cancelar la expiración establecida
$user->setExpiration(null);

Si el usuario fue desconectado debido a la expiración del intervalo de tiempo, lo indica el método $user->getLogoutReason(), que devuelve la constante Nette\Security\UserStorage::LogoutInactivity (límite de tiempo expirado) o UserStorage::LogoutManual (desconectado por el método logout()).

Autenticador

Es un objeto que verifica las credenciales de inicio de sesión, es decir, generalmente el nombre y la contraseña. Una forma trivial es la clase Nette\Security\SimpleAuthenticator, que podemos definir en la configuración:

security:
	users:
		# nombre: contraseña
		frantisek: tajneheslo
		katka: jestetajnejsiheslo

Esta solución es más adecuada para fines de prueba. Mostraremos cómo crear un autenticador que verificará las credenciales de inicio de sesión contra una tabla de base de datos.

El autenticador es un objeto que implementa la interfaz Nette\Security\Authenticator con el método authenticate(). Su tarea es devolver la llamada identidad o lanzar una excepción Nette\Security\AuthenticationException. También sería posible indicar un código de error para distinguir más finamente la situación ocurrida: Authenticator::IdentityNotFound y 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('Usuario no encontrado.');
		}

		if (!$this->passwords->verify($password, $row->password)) {
			throw new Nette\Security\AuthenticationException('Contraseña inválida.');
		}

		return new SimpleIdentity(
			$row->id,
			$row->role, // o un array de múltiples roles
			['name' => $row->username],
		);
	}
}

La clase MyAuthenticator se comunica con la base de datos a través de Nette Database Explorer y trabaja con la tabla users, donde en la columna username está el nombre de inicio de sesión del usuario y en la columna password el hash de la contraseña. Después de verificar el nombre y la contraseña, devuelve la identidad, que lleva el ID del usuario, su rol (columna role en la tabla), del que hablaremos más adelante, y un array con datos adicionales (en nuestro caso, el nombre de usuario).

Añadiremos el autenticador a la configuración como un servicio del contenedor DI:

services:
	- MyAuthenticator

Eventos $onLoggedIn, $onLoggedOut

El objeto Nette\Security\User tiene eventos $onLoggedIn y $onLoggedOut, por lo que puedes agregar callbacks que se invocarán después de un inicio de sesión exitoso o después del cierre de sesión del usuario, respectivamente.

$user->onLoggedIn[] = function () {
	// el usuario acaba de iniciar sesión
};

Identidad

La identidad representa un conjunto de información sobre el usuario que devuelve el autenticador y que posteriormente se almacena en la sesión y se obtiene mediante $user->getIdentity(). Por lo tanto, podemos obtener el id, los roles y otros datos del usuario, tal como los pasamos en el autenticador:

$user->getIdentity()->getId();
// también funciona el atajo $user->getId();

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

// los datos del usuario están disponibles como propiedades
// el nombre que pasamos en MyAuthenticator
$user->getIdentity()->name;

Lo importante es que al cerrar sesión mediante $user->logout(), la identidad no se borra y sigue estando disponible. Por lo tanto, aunque un usuario tenga una identidad, no tiene por qué haber iniciado sesión. Si quisiéramos borrar explícitamente la identidad, cerraríamos la sesión del usuario llamando a logout(true).

Gracias a esto, puedes seguir suponiendo qué usuario está en el ordenador y, por ejemplo, mostrarle ofertas personalizadas en una tienda online, pero solo puedes mostrarle sus datos personales después de iniciar sesión.

La identidad es un objeto que implementa la interfaz Nette\Security\IIdentity, la implementación predeterminada es Nette\Security\SimpleIdentity. Y como se mencionó, se mantiene en la sesión, por lo que si, por ejemplo, cambiamos el rol de alguno de los usuarios que han iniciado sesión, los datos antiguos permanecerán en su identidad hasta que vuelva a iniciar sesión.

Almacenamiento del usuario conectado

Las dos informaciones básicas sobre el usuario, es decir, si ha iniciado sesión y su identita, generalmente se transmiten en la sesión. Lo cual se puede cambiar. El almacenamiento de esta información está a cargo de un objeto que implementa la interfaz Nette\Security\UserStorage. Hay dos implementaciones estándar disponibles, la primera transmite datos en la sesión y la segunda en una cookie. Se trata de las clases Nette\Bridges\SecurityHttp\SessionStorage y CookieStorage. Puedes elegir el almacenamiento y configurarlo muy cómodamente en la configuración security › authentication.

Además, puedes influir en cómo se realizará exactamente el almacenamiento de la identidad (sleep) y la restauración (wakeup). Basta con que el autenticador implemente la interfaz Nette\Security\IdentityHandler. Esta tiene dos métodos: sleepIdentity() se llama antes de escribir la identidad en el almacenamiento y wakeupIdentity() después de leerla. Los métodos pueden modificar el contenido de la identidad o reemplazarla por un nuevo objeto que devuelvan. El método wakeupIdentity() puede incluso devolver null, lo que desconectará al usuario.

Como ejemplo, mostraremos la solución a la pregunta frecuente sobre cómo actualizar los roles en la identidad inmediatamente después de cargarla desde la sesión. En el método wakeupIdentity(), pasaremos a la identidad los roles actuales, por ejemplo, desde la base de datos:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// aquí se puede modificar la identidad antes de escribirla en el almacenamiento después del inicio de sesión,
		// pero ahora no lo necesitamos
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// actualización de roles en la identidad
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

Y ahora volvemos al almacenamiento basado en cookies. Te permite crear un sitio web donde los usuarios pueden iniciar sesión sin necesidad de sesiones. Es decir, no necesita escribir en el disco. De hecho, así funciona también el sitio web que estás leyendo ahora, incluido el foro. En este caso, la implementación de IdentityHandler es una necesidad. En la cookie solo almacenaremos un token aleatorio que represente al usuario conectado.

Primero, en la configuración, establecemos el almacenamiento deseado mediante security › authentication › storage: cookie.

En la base de datos, crearemos una columna authtoken, en la que cada usuario tendrá una cadena completamente aleatoria, única e impredecible de longitud suficiente (al menos 13 caracteres). El almacenamiento CookieStorage transmite en la cookie solo el valor $identity->getId(), por lo que en sleepIdentity() reemplazaremos la identidad original por una sustituta con authtoken en el ID, mientras que en el método wakeupIdentity(), según el authtoken, leeremos toda la identidad de la base de datos:

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);
		// verificamos la contraseña
		...
		// devolvemos la identidad con todos los datos de la base de datos
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// devolvemos una identidad sustituta, donde en el ID estará el authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// reemplazamos la identidad sustituta por la identidad completa, como en authenticate()
		$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
		return $row
			? new SimpleIdentity($row->id, null, (array) $row)
			: null;
	}
}

Múltiples inicios de sesión independientes

Es posible tener varios usuarios conectados de forma independiente dentro de un mismo sitio web y una misma sesión. Si, por ejemplo, queremos tener una autenticación separada para la administración y la parte pública en el sitio web, basta con asignar a cada una su propio nombre:

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

Es importante recordar establecer el espacio de nombres siempre en todos los lugares pertenecientes a la parte correspondiente. Si usamos presentadores, estableceremos el espacio de nombres en el ancestro común para esa parte, generalmente BasePresenter. Lo haremos extendiendo el método checkRequirements():

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

Múltiples autenticadores

La división de la aplicación en partes con inicio de sesión independiente generalmente requiere también diferentes autenticadores. Sin embargo, si registráramos dos clases que implementan Authenticator en la configuración de servicios, Nette no sabría cuál de ellas asignar automáticamente al objeto Nette\Security\User y mostraría un error. Por lo tanto, debemos limitar el autowiring para los autenticadores de modo que funcione solo cuando alguien solicite una clase específica, por ejemplo, FrontAuthenticator, lo cual logramos con la opción autowired: self:

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

Estableceremos el autenticador del objeto User antes de llamar al método login(), por lo que generalmente en el código del formulario que lo inicia sesión:

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