Json Web Token (JWT) y los errores que debes evitar

JWT 8 de jun. de 2020

Cuando desarrollas una API REST o cualquier cosa similar a una API, es necesario agregar una capa de seguridad que permita controlar el acceso a los recursos, por supuesto que durante mucho tiempo se ha utilizado diferentes opciones que trabajan muy bien en la tarea, uno de ellos se sumó hace algunos años, me refiero a los JWT (Json Web Token), veremos un poco más acerca esta tecnología, sin ignorar las bases necesarias para entender cómo funciona, pero reforzando su uso con prácticas recomendadas. Acompáñame en las siguientes líneas...

¿Qué son los JWT?

De manera sencilla, es una cadena de texto que identifica al portador del token frente al servidor. Ahora bien, este token sigue las reglas del estándar (RFC-7519) y solo pide objetos JSON para ser creado. La mayoría de desarrolladores lo utiliza para autenticar usuarios en aplicaciones web o móviles y manejar sus sesiones de esa forma, hasta debo aceptar que yo lo he hecho hace muy poco, pero es más útil cuando la utilizas para permitir el acceso entre servidores, por ejemplo, cuando accedes a un microservicio que solo genera archivos PDF, es allí donde le estas dando el mejor uso, más adelante sabremos por qué...

Estructura básica

Un token puede verse un poco caótico e incluso un JWT puede parecer serlo, sin embargo, podemos entenderlo gracias a la división de su estructura:

eyJhbGciOiJIU . eyJzdWIiOiIxMjM0MDIyfQ . SflKxwRJSMeadQssw5c

Por si aún no lo has notado, se puede representar mejor en la siguiente imagen:

Estructura JWT. Fuente: tutorialedge.net

El token está compuesto por un header, un cuerpo o payload y la firma del mismo, cada uno esta encriptado por un algoritmo.

  • Header: Objeto JSON encriptado con base64url.
  • Cuerpo o payload: Objeto JSON encriptado con base64url.
  • Signature: String compuesto por (header + payload + secret) y encriptado comúnmente por HMACSHA256.

Componentes

Veamos un poco más en detalle:

El objeto incluye la información del tipo de encriptación que se utiliza para firmar el token, su forma mas básica es la siguiente:

{
  "alg": "HS256",
  "typ": "JWT"
}

Ahora, estas son las opciones que puedes agregar:

  • alg: Tipo de algoritmo utilizado para firmar el token (HMAC, SHA256, RSA, entre otros).
  • typ: Tipo de token(en este post solo trataremos JWT).

Body

Este objeto incluye la información personalizada y campos adicionales llamados preocupaciones o claims, que son campos para definir el comportamiento de un token, en conjunto los podemos ver como en el siguiente ejemplo:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Ahora, las opciones están clasificadas en 3 grupos:

Opciones registradas

Según IANA(institución que define los nombres técnicos a recursos de internet) las opciones disponibles son las siguientes:

  • iss: Emisor del token, puede ser un string o uri.
  • sub: Asunto del token, puede ser un string o uri, en este campo puede ir el identificador de un cliente.
  • aud: Identifica el nombre del servidor de recursos, por ejemplo: misuperapi.com
  • exp: Fecha o fecha y hora(numericDate, int, unix timestamp) que indica cuando el token deja de ser válido.
  • nbf: Fecha o fecha y hora(numericDate, int, unix timestamp) que indica desde cuando token puede ser utilizado.
  • iat: Fecha o fecha y hora(numericDate, int, unix timestamp) que indica cuando fue emitido el token, ayuda a saber la antigüedad del mismo.
  • jti: Identificador único del token, se puede utilizar para identificar que token deben ser incluido en una especie de lista negra.

Opciones públicas

Están definidas en el registro de IANA, sin embargo son resistentes a sobreescritura, es decir cualquier valor que sea asignado no debe causar ningún problema, por ejemplo: name, email, family_name, nickname, profile, picture, website, entre otros, la lista completa esta aquí.

Opciones privadas

No son ni públicas ni tampoco registradas, es decir son claves personalizadas, por ejemplo "misuperclave" aplica en este grupo.

Sobre la firma

La firma(signature) es la parte que permite verificar la idoneidad del contenido del token, se asegura de que no ha sido alterado. Ahora bien, como se mencionó anteriormente está compuesto por la combinación de (header + payload + secret) de la siguiente manera:

HMACSHA256(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    secret // también puedes pasarlo antes por base64Url
)

Nótese que se está utilizando el algoritmo HMACSHA256 para firmar el token, sin embargo no es el único, pueden utilizarse otros como HS384, HS512, RS256 inclusive setearle "none" (veremos algo sobre este dentro de un momento), aquí puedes revisar la lista apta de algoritmos para trabajar con los JWT.

El secret puede ser un texto cualquiera que sea complicado de descifrar, y es más, puedes encodearlo a base64url antes de pasarlo al algoritmo de encriptación, esto último es opcional pero puedes hacerlo. Finalmente, tal vez debería ya asumirse pero de igual forma hay que recalcarlo, el secret va tu servidor y NO debe ser compartido ni deben conocerlo tus usuarios.

Flujo simple

Todos los procesos tienen un inicio y un final, y para entender de la forma más simple al JWT, olvidándonos de todas las preocupaciones, utilizaremos la siguiente imagen:

Flujo básico JWT. Fuente: vaadata.com

Solo necesitamos a dos actores para reflejar el flujo, el primero que simplemente puede ser el navegador o cualquier otro cliente (un móvil también puede servir), y el servidor que puede ser cualquier backend escrito en el lenguaje de tu preferencia php, python, javascript, ruby, etc.

Para generar el token

El proceso empieza cuando el cliente se identifica con su usuario y contraseña(1), el backend verifica que esos datos sean correctos, si lo es, crea un token(2) (como lo explicamos en el apartado anterior) y lo devuelve al cliente(3).

Para utilizar el token

Una vez que tenemos el token, lo utilizamos en todas las solicitudes que necesiten que el cliente se encuentre autenticado, enviándolo en el header(4). El backend, por su parte, lo recibe y verifica que sea válido(5), si lo es, devuelve el recurso que se solicitó en la petición(6), que bien pudo ser una imagen, un video, data en stream, o simplemente un json, etc.

Así que solo podrás acceder a un recurso protegido si el token es enviado de alguna manera, comúnmente es ser enviado en el header de la petición de la siguiente forma:

Authorization: Bearer <token>

¿Que pasa si falla la validación del token?

En la imagen del flujo no se muestra, pero lo que sucede es que simplemente te impide acceder al recurso y el servidor debería devolver una respuesta diciendo que no está autorizado (403).

Ejemplo básico para codificar / descodificar tu propio JWT

No podemos hacer un post sin un ejemplo práctico que respalde lo que se menciona aquí y que nos lleve un poco más a entender que hay por debajo de la abstracción de las librerías, así a mano, haremos algo sencillo para que tú mismo construyas tu propio generador de tokens super básico.

const crypto = require('crypto'); // 1

const secret = 'eldevsin.site'; //2

const toBase64 = data => { // 3
  const buffer = Buffer.from(JSON.stringify(data));

  return toBase64URL(buffer.toString('base64'));
};

const toBase64URL = base64 => { // 4
  return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
};

const getSignature = (header, body) => { // 5
  const data = header + '.' + body;

  const hmac = crypto.createHmac('sha256', secret);

  return toBase64URL(hmac.update(data).digest('base64'));
};

Avancemos un poco a ver de lo que se trata ésta primera parte:

  1. Importamos el paquete crypto, es nativo de node y provee la funcionalidad criptográfica.
  2. Este es el secret o algunos lo conocen como semilla, es solo un string que nos servirá para crear la firma más adelante.
  3. toBase64: La función que creamos y que nos permite convertir la data(comúnmente puede ser un texto u objeto) a base64.
  4. toBase64URL: Espera un base64, para convertirlo a base64URL, ¿cómo?reemplazando ciertos caracteres.
  5. getSignature: Función que espera la cabecera y el cuerpo del token(ya en base64URL), para firmarlo utilizando el algoritmo de sha256 con ayuda del secret, finalmente antes de devolverlo se codifica a base64URL.
const encode = (header, body) => { // 6
  const headerB64 = toBase64(header);
  const bodyB64 = toBase64(body);

  return headerB64 + '.' + bodyB64 + '.' + getSignature(headerB64, bodyB64);
};

const decode = (jwt) => { // 7
  const chunks = jwt.split('.');

  if (chunks.length != 3) {
    throw new Error('Something is wrong.');
  }

  const [header, body, signature] = chunks;

  const newSignature = getSignature(header, body);

  if (newSignature != signature) {
    throw new Error('invalid token.');
  }

  return JSON.parse(Buffer.from(body, 'base64').toString('utf8'));
};

Para esta segunda parte acabamos de crear sólo dos funciones:

  1. encode: Función que abstrae la codificación del token, (header + body + signature).
  2. decode: Función para la decodificación del token, para eso separa el string en 3 partes(header, body, signature), de esas partes sólo tomamos el header y body para generar nuevamente la firma y compararla con la que se extrajo, si coincide devolvemos el contenido del cuerpo, si no, tiramos un error.

La implementación es igual de sencilla:

const bodyExample = {
  sub: '1234567890',
  name: 'John Doe',
  iat: Date.now(),
};

const headerExample = {
  alg: 'HS256',
  typ: 'JWT',
};

const jwt = encode(headerExample, bodyExample);

console.log('Token: ', jwt);

const data = decode(jwt);

console.log('Información:', data);

El resultado es similar a la siguiente imagen:

Recuerda que este ejemplo NO esta hecho para ser usado en producción, sin embargo puedes considerarlo como la base y mejorarlo aplicándole más validaciones, este código está muy cerca de igualar a la librería jwt-simple (que por cierto tiene unas cifras interesantes), a si que, con un poco más de trabajo incluso la puedes superar en medidas de seguridad.

Llevemos a buen puerto el uso de JWT con buenas prácticas que puedes aplicar...

Prácticas recomendadas

Los JWT pueden ser un mecanismo de identificación con diversas ventajas, pero..¿qué sucede si este mecanismo no es correctamente implementado?, sencillo, probablemente tendrás una bandeja de entrada explotando por la cantidad de incidentes reportados.

Por eso es importante realizar una especie de checklist y aplicar cada uno de ellos en la medida de lo posible para agregar un poco más de peso en términos de seguridad.

Escribe un secret largo y complicado

No se te ocurra agregar la típica contraseña "del 1 al 9", solo le harás la vida más fácil para que alguien aplique fuerza bruta con herramientas como ésta, es mejor agregar un secret que contenga entre 16 bytes y 128 bytes para el encriptado HS256, y entre 32 bytes a 128 bytes para el HS512.

No confíes en el tipo de algoritmo

Es mejor forzar la lectura de un token con un algoritmo específico como HS256, que confiar en el valor que viene en "alg" dentro del header, con eso previenes que alguien te envíe seteado el valor "none".

Tal vez pienses que no tiene nada de malo enviar el encabezado en "alg": "none", sin embargo, evita que el token sea validado, pasando la seguridad de tu aplicación como una coladera y tus usuarios pasando a ser víctimas de la INseguridad. A menos que tengas otra medida para validar la integridad de token, te recomiendo evitar confiar en lo que recibes.

Expira el token lo más pronto que puedas

Dependiendo del tipo de aplicación donde se implementa JWT debes asignar un tiempo de expiración, poniéndonos extremos no es lo mismo tener un token para una aplicación bancaria que uno para un todo list.

Te pido encarecidamente que evites crear tokens sin fecha de caducidad cuando sea posible.

No agregues datos sensibles

Esto aplica para todos los casos, es super básico evitar agregar información sensible en el payload del JWT como contraseñas, datos bancarios o similares, debido a que el contenido es sencillo de obtener... codificar en base64 no es lo mismo que encriptar.

Otros datos a verificar

La verificación de estos datos se hacen en backend y depende de qué opciones estás utilizando en el token.

  • iat: se encuentre antes del momento actual.
  • exp: no sea mayor que el momento actual.
  • iss: Sea un emisor válido.
  • aud: Que la audiencia corresponda al de tu servidor.
  • Tus claims personalizados: Cualquier opción privada que venga del token debes validarla y debe corresponde con los valores aceptados en tu backend.

Librerías utilizadas

Como ya saben este blog y su servidor apoya deliberadamente el uso de node para hacer innumerables pruebas, ejercicios, etc., y las siguientes librerías son de este mismo runtime y además son las recomendadas por jwt.io:

  • Jsonwebtoken: Implementación de JsonWebToken para node.js.
  • Jose: "JSON Web casi todo" JWA, JWS, JWE, JWK, JWT, JWKS para Node.js con dependencias mínimas.

Hasta este momento posiblemente salga a flote la siguiente pregunta...

¿Con todo esto... nos olvidamos de las cookies?

Las cookies son una tecnología que ha estado presente durante mucho tiempo, y definitivamente es algo que funciona muy bien, con los pros y contras que ello representa...

Pongamonos en este contexto...¿qué sucede si tienes una aplicación de múltiples roles y el administrador quiere cambiar de rol a un usuario con sesión activa?, si utilizaste cookies simplemente estamos super bien, pues solo tenemos almacenado el ID sesión y al ser un sistema con estado(statefull) consultaremos el almacenamiento en cada petición(puede ser una BD como postgres, mongoDB, o un redis, ¡que se yo!), sin embargo, si utilizaste JWT es muy probable que quieras ser fiel y mantener su característica sin estado(stateless), así que el rol lo metiste dentro del token...¡uy! groso error, ahora tendrás que esperar hasta la expiración del token para restringir su acceso. Este no es un buen caso para utilizar JWT.

Por otro lado, tenemos un caso donde queremos utilizar un servicio de servidor a servidor, por ejemplo para convertir imágenes a pdf (no me preguntes por qué solo es un ejemplo) allí no tenemos que estar consultando a la base de datos para saber quién es el que quiere acceder a los recursos o ver algún perfil, y por ende no se pierde la característica de "sin estado", las cookies no serían una buena opción aquí.

Entonces, NO podemos simplemente reemplazar el uso de cookies por el JWT sin pensar el ¿por qué?, por supuesto que un proyecto que no necesite el uso de JWT estará condenado a incrementar su complejidad innecesariamente.

Boom! Tal vez no lo esperabas, y estoy convencido que este punto se merece hablar en un post completamente propio, sin embargo voy a adelantarte un poco sobre este tipo de enfoque. Cuando trabajas con tokens hay formas de evitar almacenar el JWT en el localStorage, ¿y por qué evitarlo? fácil, el local storage puede ser accedido por cualquier script que sea propio de la web o no, eso es peligroso.

Entonces, una alternativa interesante es almacenar el token en una cookie, y ésta debe ser segura (httpOnly), porque de esa forma no es manipulable por javascript y nos protegemos de ataques XSS, pero debes tener en cuenta que el tamaño máximo es de 4k por tanto el contenido no debe superar este límite.

De este enfoque hay algunas variaciones completamente válidas que usan cookies para potenciar el uso de token, obviamente no todos los proyectos pueden utilizar esta práctica pero es una forma de utilizar lo bueno de ambos mundos, de hecho es un muy buen tema para verlo en un post más detallado.


Sin duda utilizar JWT es una ola que se viene utilizando a lo largo de la web, trae consigo una forma interesante para verificar identidades y accesos a recursos pero a su vez nuevas preocupaciones se suman al proyecto, sabemos que es útil en muchos casos y da cierta comodidad para manipular sus opciones utilizando objetos literales o JSON.

Por otra parte hay que tener en cuenta las prácticas recomendadas para no dejar al azar los puntos de seguridad, refuerza este punto con el checklist que te dejamos anteriormente.

Algo que siempre recalcamos en este blog es que consideres que cualquier tecnología no es la panacea y debemos evaluar utilizarla(o no) con criterio.

Lo obligatorio

Déjame un comentario si consideras que este post te aportó valor, y cuéntame si ya estás usando JWT en tus proyectos y como lo estás haciendo, no olvides seguir el blog en nuestras redes sociales y apuntarte al newsletter, nos vemos en una próxima entrega que seguro te sorprenderá.

Por GIPHY

Etiquetas

¡Genial! Te has suscrito con éxito.
¡Genial! Ahora, completa el checkout para tener acceso completo.
¡Bienvenido de nuevo! Has iniciado sesión con éxito.
Éxito! Su cuenta está totalmente activada, ahora tienes acceso a todo el contenido.