Introducción a JWT (JSON Web Token)

Una vez que un usuario se ha autenticado, ya puede, en principio, moverse libremente por nuestra aplicación o, al menos, por aquellas partes de la misma para las que esté autorizado. Cada vez que intente acceder a una funcionalidad de la aplicación, se debe verificar no sólo que sea un usuario legítimo, si no que tenga permiso para entrar a esa funcionalidad específica.

El sistema tradicional es mediante sesiones. Es un sistema que se mantiene en boga en muchos sitios actualmente. Sin embargo, no todos los lenguajes de backend gestionan sesiones y, además, presenta un posible agujero de seguridad si, al cerrar el navegador, la sesión no es adecuadamente destruida. La alternativa es mediante el uso de cookies. Sin embargo, esto es aún peor. Al margen de la cuestión legislativa de que el usuario debe conocer y aceptar el uso de cookies, tener datos como el login y la contraseña en cookies, que van a permanecer en el ordenador del usuario incluso tras cerrar el navegador no es, desde luego, la mejor idea del mundo. Las cookies están bien para, por ejemplo, almacenar siertas preferencias de navegación del cliente, pero no para los datos de autenticación.

JWT (JSON Web Token) ofrece una solución que tiene las ventajas de las sesiones y las cookies, pero carece de sus inconvenientes, proporcionando una capa extra de seguridad. Vamos a conocerlo en este artículo.

QUÉ ES JWT

JWT consiste en crear un token con los datos que decidamos encriptar del usuario, mediante una clave privada alojada en el servidor, de modo que, en cada acceso a una página protegida, se verifique que la persona que intenta acceder es el usuario que se supone que es. Este sistema está cada vez más difundido por las posibilidades de seguridad que ofrece. Productos tan relevantes como Angular y otros frameworks, además de los lenguajes más populares, tanto de frontend como de backend, lo emplean cada día más. Por algo será.

JWT recopila los datos a partir de los que deseamos generar el token. Puede ser un nombre de usuario, o el nombre y su contraseña, o incluir el role que tenga el usuario… Eso dependerá de los niveles de seguridad de que queramos dotar a nuestra aplicación. Además, se suelen incluir dos datos muy específicos: el momento en que se crea el token, y el tiempo de validez del mismo. Esto nos proporciona un nivel adicional de seguridad ya que, una vez expire el plazo establecido, el usuario deberá volver a identificarse.

Una vez que tenemos los datos que consideramos necesario para el nivel de seguridad deseado en nuestra aplicación se codifican en JSON… Bueno, no exactamente en JSON: se codifican en lo que se conoce como BSON (Binary JSON). Es como codificar en JSON y binarizar esa codificación. Además, la codificación se hace en base a la clave privada que mencionábamos hace un momento (recuerda, alojada en el servidor), de modo que, el mismo paquete original de datos, no podrá ser descodificado, y no se podrán recuperar los datos, si no se conoce esa clave.

LA LIBRERÍA JWT

Para trabajar en PHP con JWT contamos con la librería Firebase JWT. Esta podemos descargarla desde este enlace, o bien instalarla mediante composer, tecleando, en la terminal de mandatos en el directorio raíz de nuestro sitio, lo siguiente:

composer require firebase/php-jwt

Lo siguiente que debemos hacer es incluirla en el script donde vayamos a generar nuestro token, o bien a decodificarlo para leer su contenido. Una vez más, hay dos modos de hacer esto. Si has descargado la librería desde el enlace de GitHub y has incluido el directorio src (con ese u otro nombre) en la estructura de tu proyecto «a pelo», en el script donde vayas a usar la clase JWT deberás incorporar lo siguiente:

require_once './src/JWT.php';

use Firebase\JWT\JWT;

ATENCIÓN. Recuerda dejar siempre una línea en blanco antes y después de la línea con la instrucción use. Aunque esto es algo que no está oficialmente documentado en ninguna parte, si no lo haces así el intérprete de PHP podría fallar (y de hecho falla en ocasiones), en determinados contextos de desarrollo. Es un comportamiento agnogénico, aleatorio (lo que significa que no obedece a causas tipificables) y es bastante molesto.

Si has incorporado esta librería mediante composer (lo que, desde luego, es mucho más aconsejable), la forma de incorporar la clase JWT a tu script es la siguiente:

require_once './vendor/autoload.php';

use Firebase\JWT\JWT;

A continuación definiremos tres variables, que usaremos para construir el token y, posteriormente, descifrarlo recuperando los datos originales. Estas serán:

  • El momento actual, en el que se va a crear el token. Se puede obtener directamente con la función time() de PHP que, como sabes, te devuelve el número de segundos transcurridos desde el comienzo de la Era Unix (las cero horas del 1 de Enero de 1970).
  • La duración de vida del token, expresada en segundos. Si el token intenta verificarse pasado ese lapso de tiempo, el sistema lo reconocerá como expirado y, por lo tanto no podrá descodificarse ni usarse para nada.
  • La clave privada que se va a emplear para codificar y descodificar el token.

Estos datos los podemos incluir así:

$currentTime = time();
$limitTime = $currentTime + 3600;
$privateKey = 'XAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9';

Con esto definiremos un token cuyo lapso de vida será de una hora (3600 segundos).

A continuación crearemos una matriz con estos datos, y con los datos del usuario que deseemos incluir en el token. Por ejemplo, podría ser algo así:

Esta es la forma típica de crear la matriz con los datos para el token. Observa especialmente las líneas resaltadas, antes de los datos del usuario que se autentica, con las claves iat y exp, para establecer el momento de creación y el momento límite de vida del token. Es importante que estos dos elementos estén, como ves en el ejemplo, al principio de la estructura, y que tengan esas claves. De otro modo, el control de duración del token no funcionará.

A partir de estos datos, y de la clave privada, podemos crear el token, así:

$tokenJWT = JWT::encode($tokenContent, $privateKey);

El método estático JWT::encode() recibe dos parámetros. El primero es la matriz de los datos que queremos convertir en token. El segundo es la clave privada que empleamos para la conversión.

Con esto crearemos el token, que podrá ser pasado a otro proceso o a cualquier artefacto encargado de descifrarlo y autenticar el usuario. Para ello, el artefacto encargado de descodificar el token deberá conocer, como ya hemos comentado, la clave privada. Sin esta, es imposible la resuperación de los datos originales. Para la descodificación empleamos el método estático JWT::decode(), así:

$dataObject = JWT::decode($tokenJWT, $privateKey, array('HS256'));

Este método debe recibir tres parámetros:

  • El token a partir del cual queremos recuperar los datos.
  • La clave privada con la que fueron codificados los datos. Si no coincide, no podremos recuperarlos.
  • Una matriz con un elemento referido al algoritmo empleado para la descodificación. En seguida entraremos en detalles sobre ese punto.

Esto nos devuelve la matriz con los datos, en forma de objeto, así:

Si queremos obtener la matriz original en formato «natural», no de objeto, podemos, a continuación, hacer algo tan simple como lo siguiente:

$dataArray = json_decode(json_encode($dataObject), true);

Y ya la tenemos:

LOS ALGORITMOS DE DESCODIFICACIÓN

La librería JWT admite seis algoritmos diferentes de codificación / descodificación, de los que tres son para conexiones sin SSL y los otros tres para conexiones SSL. Dentro de esta clasificación, el algoritmo empleado dependerá del nivel de seguridad que queramos aportar al token. A mayor algoritmo, mayor nivel de seguridad, pero también mayor costo en procesamiento por parte del sistema. Los seis algoritmos posibles son:

  • Sin SSL:
    • HS256
    • HS384
    • HS512
  • Con SSL:
    • RS256
    • RS384
    • RS512

El algoritmo por defecto es HS256. Si deseamos usar otro, debemos especificarlo no solo en la descodificación, sino también en la codificación, añadiendo un tercer parámetro que, hasta ahora, no hemos usado. Por ejemplo, si deseamos emplear el algoritmo HS512 deberemos hacer la codificación (la obtención del token), así:

$tokenJWT = JWT::encode($tokenContent, $privateKey, 'HS512');

La descodificación la haremos así:

$dataObject = JWT::decode($tokenJWT, $privateKey, array('HS512'));

Cuando empleamos el algoritmo HS256 no es necesario incluirlo en la codificación porque es el valor por defecto, aunque sí debemos indicarlo, ineludiblemente, en la descodificación. Si empleamos otro algoritmo, debe ir especificado en ambos procesos.

En la descodificación podemos establecer más de un algoritmo, así:

$dataObject = JWT::decode($tokenJWT, $privateKey, array('HS256', 'HS512'));

Esto nos descodificará el token, tanto si ha sido codificado con el valor por defecto (HS256), como si se ha codificado con HS512. Sin embargo, recuerda que en la codificación, como es lógico, sólo puedes establecer un algoritmo (si no estableces ninguno, como hemos hecho en el apartado anterior), se asume el algoritmo por defecto. Entre los que se mencionan en la descodificación, deberá aparecer el que se haya usado en la codificación. Si no, no funcionará. Una manera de asegurarte de que siempre vas a poder emplear el algoritmo correcto es especificar los seis en la descodificación, así:

$dataObject = JWT::decode($tokenJWT, $privateKey, array('HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512'));

Sin embargo, recuerda que los tres últimos sólo son operativos si estás trabajando bajo OpenSSL (https, para entendernos).

CONCLUYENDO

En este artículo hemos aprendido lo que necesitaremos en la mayoría de los casos para emplear la autenticación por JWT. El ejemplo completo puedes descargarlo en este enlace. Sin embargo, hay otras detalles respecto a esta técnica, que conoceremos en el siguiente artículo.

   

Deja un comentario