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

JWT jun. 08, 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

Fernando Palacios

¡Hoy es un gran día para hacer grandes cosas!

¡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.