Autenticación de usuarios

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

  • iniciar y cerrar la sesión de los usuarios
  • autenticadores propios

Instalación y requisitos

En los ejemplos usaremos un objeto de la clase Nette\Security\User, que representa al usuario actual y que obtiene haciendo que se lo pasen mediante dependency injection. En los presenters basta con llamar a $user = $this->getUser().

Autenticación

Autenticación significa inicio de sesión del usuario, es decir, el proceso durante el cual se verifica la identidad del usuario. El usuario suele identificarse con un nombre de usuario y una contraseña. La verificación la realiza el llamado Autenticador. Si el inicio de sesión falla, se lanza una Nette\Security\AuthenticationException.

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

Así se cierra la sesión del usuario:

$user->logout();

Y para averiguar si el usuario tiene la sesión iniciada:

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

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

En los presenters puede verificar el inicio de sesión en el método startup() y redirigir a los usuarios sin sesión iniciada a la página de inicio de sesión.

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

Expiración

La sesión del usuario caduca junto con la expiración del almacenamiento, que suele ser la sesión (vea el ajuste de la expiración de la sesión). Pero también puede establecer un intervalo de tiempo más corto tras el cual se cierra la sesión del usuario. Para eso sirve el método setExpiration(), que se llama antes de login(). Pase como argumento una cadena con un tiempo relativo:

// la sesión caduca tras 30 minutos de inactividad
$user->setExpiration('30 minutes');

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

El método $user->getLogoutReason() revela si la sesión del usuario se cerró porque expiró el intervalo de tiempo. Devuelve o bien la constante Nette\Security\User::LogoutInactivity (expiró el límite de tiempo), o bien User::LogoutManual (se llamó al método logout()).

Autenticador

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

security:
	users:
		# nombre de usuario: contraseña
		johndoe: 'secret123'
		kathy: 'evenmoresecretpassword'

En lugar de las contraseñas en texto plano puede indicar también sus hashes; vea la configuración.

Esta solución es más adecuada para hacer pruebas. Le mostraremos cómo crear un autenticador que verifique las credenciales de inicio de sesión contra una tabla de la base de datos.

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

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

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

El autenticador lo añadiremos a la configuración como servicio del contenedor DI:

services:
	- MyAuthenticator

Eventos $onLoggedIn, $onLoggedOut

El objeto Nette\Security\User tiene los eventos $onLoggedIn y $onLoggedOut, así que puede añadir callbacks que se disparan tras un inicio de sesión correcto o tras cerrar la sesión el usuario, respectivamente.

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

Identidad

Una identidad es un conjunto de información sobre el usuario que devuelve el autenticador y que después se guarda en la sesión y se puede obtener con $user->getIdentity(). Eso nos permite 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 accesibles como propiedades
// el nombre de usuario que pasamos en MyAuthenticator
$user->getIdentity()->name;

Lo importante es que, al cerrar la sesión con $user->logout(), la identidad no se borra y sigue estando disponible. Así que, aunque un usuario tenga identidad, no tiene por qué tener la sesión iniciada. Si queremos borrar la identidad explícitamente, cerramos la sesión del usuario llamando a logout(true).

Gracias a eso puede seguir suponiendo qué usuario está frente al ordenador y mostrar, por ejemplo, ofertas personalizadas en una tienda electrónica, pero su información personal solo se la puede mostrar después de que inicie sesión.

Además de borrar la identidad en cada llamada con logout(true), puede desactivar por completo su conservación con la propiedad $persistIdentity. Cuando se establece a false, la identidad se descarta en cada cierre de sesión y en la expiración, así que getIdentity() devuelve entonces null. Conservar la identidad depende también del almacenamiento: el almacenamiento en cookie no puede conservarla tras el cierre de sesión, porque siempre borra la cookie.

Una identidad es un objeto que implementa la interfaz Nette\Security\IIdentity. La implementación predeterminada es Nette\Security\SimpleIdentity. Y, como se ha dicho, se mantiene en la sesión, así que si, por ejemplo, cambiamos el rol de alguno de los usuarios con la sesión iniciada, los datos antiguos seguirán en su identidad hasta que vuelva a iniciar sesión.

Almacenamiento del usuario con sesión iniciada

Las dos informaciones básicas sobre el usuario, es decir, si tiene la sesión iniciada y su Identidad, se transmiten normalmente en la sesión. Lo cual se puede cambiar. De guardar esta información se encarga un objeto que implementa la interfaz Nette\Security\UserStorage. Hay disponibles dos implementaciones estándar: Nette\Bridges\SecurityHttp\SessionStorage, que transmite los datos en la sesión, y CookieStorage, que los transmite en una cookie. El almacenamiento se puede elegir y configurar muy cómodamente en la configuración security › authentication.

Además puede influir en cómo transcurren exactamente el guardado (sleep) y la restauración (wakeup) de la identidad. Lo único que hace falta es que el autenticador implemente la interfaz Nette\Security\IdentityHandler. El método sleepIdentity() se llama antes de escribir la identidad en el almacenamiento, y wakeupIdentity() después de leerla. Estos métodos pueden modificar el contenido de la identidad o sustituirla por un objeto nuevo que devuelven. El método wakeupIdentity() incluso puede devolver null, lo que cierra la sesión del usuario. La interfaz declara además el método getGuestIdentity(), vea Identidad de invitado.

Como ejemplo, mostremos la solución a la pregunta frecuente de cómo actualizar los roles de la identidad justo después de cargarla de la sesión. En el método wakeupIdentity() pasamos a la identidad los roles actuales, p. ej. 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 tras el inicio de sesión,
		// pero ahora no lo necesitamos
		return $identity;
	}

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

	public function getGuestIdentity(): ?IIdentity
	{
		// aquí no se usa ninguna identidad de invitado
		return null;
	}

Volvamos ahora al almacenamiento basado en cookies. Permite crear una web donde los usuarios pueden iniciar sesión sin necesitar sesiones. Así que no necesita escribir en el disco. Así funciona la web que está leyendo ahora mismo, incluido el foro. En este caso, implementar IdentityHandler es una necesidad. En la cookie guardaremos solo un token aleatorio que representa al usuario con la sesión iniciada.

Primero, establezca en la configuración el almacenamiento requerido con security › authentication › storage: cookie.

En la base de datos, cree la columna authtoken, donde cada usuario tendrá una cadena completamente aleatoria, única e imposible de adivinar de longitud suficiente (al menos 13 caracteres). CookieStorage transmite en la cookie solo el valor $identity->getId(), así que en sleepIdentity() sustituimos la identidad original por una identidad proxy que contiene el authtoken en el ID. Al revés, en el método wakeupIdentity() leemos de la base de datos toda la identidad a partir del 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);
		// verifica la contraseña
		// ...
		// devuelve 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
	{
		// devuelve una identidad proxy donde el ID contiene el authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// sustituye la identidad proxy 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;
	}

	public function getGuestIdentity(): ?IIdentity
	{
		// aquí no se usa ninguna identidad de invitado
		return null;
	}
}

Identidad de invitado

A veces viene bien que los visitantes que no han iniciado sesión tengan también una identidad, por ejemplo para darles un conjunto predeterminado de roles o algunos datos. Si el autenticador implementa IdentityHandler, puede proporcionarla mediante el método getGuestIdentity(), que se usa siempre que nadie tiene la sesión iniciada. Entonces getIdentity(), getId() y getRoles() recurren a ella, así que los invitados pueden tener sus propios roles en lugar de solo el rol guest a secas. Devuelva null si no quiere una identidad de invitado.

public function getGuestIdentity(): ?IIdentity
{
	return new SimpleIdentity('guest', ['guest'], ['name' => 'Guest']);
}

La identidad de invitado no se guarda nunca en el almacenamiento, y el inicio de sesión siempre la sustituye.

Varios inicios de sesión independientes

Es posible tener varios usuarios independientes iniciando sesión a la vez dentro de una misma web y de una misma sesión. Por ejemplo, si queremos tener autenticación separada para la administración y para la parte pública de la web, basta con establecer para cada una un namespace único:

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

Es importante acordarse de establecer el namespace siempre en todos los sitios que pertenecen a la parte correspondiente. Si usamos presenters, establecemos el namespace en el antecesor común de esa parte, normalmente BasePresenter. Lo hacemos ampliando el método checkRequirements():

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

Si cambia el namespace durante una misma petición (después de haber leído ya el estado de autenticación), el objeto User sigue teniendo en caché el estado del namespace anterior. En ese caso, llame a refreshStorage() para descartar la caché y forzar una recarga desde el nuevo namespace:

$user->getStorage()->setNamespace('admin');
$user->refreshStorage(); // recarga el estado desde el nuevo namespace

Varios autenticadores

Dividir una aplicación en partes con inicio de sesión independiente suele requerir además autenticadores distintos. Pero si registrásemos en la configuración de servicios dos clases que implementan Authenticator, Nette no sabría cuál asignar automáticamente al objeto Nette\Security\User y mostraría un error. Por eso tenemos que restringir el autowiring de los autenticadores para que funcione solo cuando alguien pida una clase concreta, p. ej. FrontAuthenticator. Eso se consigue eligiendo autowired: self:

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

El autenticador del objeto User lo establecemos antes de llamar al método login(), así que normalmente en el código del formulario que inicia la 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.x