JWT: The right way

July 06, 2020

Introducción

JWT es un formato estandarizado para representar claims, información sobre identidad o privilegios, con el objetivo de facilitar el intercambio mediante canales con limitantes al tamaño como son las HTTP Authorization headers, las URI query parameters o los POST parameters.

JWT se planteó inicialmente como alternativa a SAML buscando reducir la verbosidad y complejidad asociadas a este por estar basado en XML y SOAP.

La información se representa mediante objetos JSON, que pueden estar firmados(JWS) o encriptados(JWE), y se codifica mediante base64Url.

De las dos implementaciones posibles de JWT la más extendida es JWS al punto de que muchas veces no se hace diferencia entre las dos especificaciones.

El resto del artículo va a estar centrado en JWS y, en otro artículo, se profundizará JWE.

Estructura

Los tokens JWS se dividen en tres partes:

  • Header
  • Payload
  • Signature

Las dos primeras son objetos JSON y la última un hash1 de las dos anteriores. Para facilitar el transporte del token las tres partes están codificadas utilizando Base64url y unidas mediante un punto quedando una cadena con el siguiente formato:

hhhhhhhhh.ppppppppp.sssssssss

La cabecera se compone generalmente de dos campos:

  • alg: Indica cuál de los algorítmos soportados fue utilizado para generar la firma. Un listado de las posibilidades está definida en el standard JWA.
  • typ: Este parámetro es opcional respecto de la implementación pero en caso de estar definido se recomienda que tenga como valor 'JWT'.

El payload contiene los claims del token y existen tres tipos: registrados, privados y públicos. Los primeros son nombres que estan reservados por la especificación mientras que los últimos son los definidos por la aplicación.

Los siguientes campos son reservados:

  • iss: Issuer, opcional. Identifica al emisor del token. Puede ser un URI o un string case-sensitive.
  • sub: Subject, opcional. Identifica el objetivo del token (nombre de usuario, recurso, étc). Puede ser un URI o un string case-sensitive.
  • aud: Audience, opcional. Identifica para quién fue emitido el token. Puede ser un URI o un string case-sensitive.
  • exp: Expiration Time, opcional. Fecha/tiempo de vencimiento del token. Numérico en segundos desde el 1970-01-01T00:00:00Z UTC.
  • nbf: Not Before, opcional. Fecha/tiempo de inicio de la validez del token. Numérico en segundos desde el 1970-01-01T00:00:00Z UTC.
  • iat: Issued At, opcional. Fecha/timepo de emisión del token. Numérico en segundos desde el 1970-01-01T00:00:00Z UTC.
  • jti: JWT ID, opcional. Identificador global único del token. String case-sensitive.

Los claims públicos son aquellos registrados en el IANA JSON Web Token Registry por especificaciones basadas en JWT como OpenID.

Por último, los privados son los internos o acordados entre partes y no se encuentran registrados o reservados.

La firma es el hash que se genera sobre la unión del contenido de la cabecera y el payload. Su función es asegurar la integridad de los datos y, en caso de utilizar uno asimétrico también permite validar el origen del token.

Opcionalmente, si no nos interesa firmar los datos, el campo alg se puede setear con el valor none que indica que no se utilizó ningún algoritmo y dejar en blanco la firma:

hhhhhhhhh.ppppppppp.

Por especificación las implementaciones de JWT únicamente estan obligadas a soportar none y HMAC. Se recomienda además que proveean soporte para RSASSA-PKCS1-v1_5 y ECDSA usando una curva P-256. En todos los casos el resultado debe ser codificado con SHA-256.

Los demás algorítmos son opcionales respecto del standard.

Como se menciono antes, el algorítmo elegido para codificar los datos es el Base64url, una variante del conocido Base64 que reemplaza los caracteres '+' por '-' y '/' por '_', resultando así en una salida del algorítmo que no utliza caracteres considerados significativos en las URL.

Casos de Uso

De forma amplia la idea es permitir el intercambio de información no sensible(por lo menos con JWS) entre partes de la que haga falta poder validar la integridad y, opcionalmente, el origen.

El subconjunto más común de esto es la identificación de un cliente tanto hacia lo interno como para con otros servicios (SSO).

Una vez que el usario está autenticado, las siguientes peticiones utilizaran un token en vez de transmitir las credenciales o almacenarlas en una sesión en el servidor.

Casos de No Uso

Como complemento a lo anterior existen usos, comunes lamentablemente, que no son los recomendados para JWT.

Si la información del token es insuficiente para resolver la petición del cliente y hace falta realizar consultas a la BBDD o a un servicio de autorización para recabar información adicional puede ser mejor centralizar la autenticación en el servidor y no desdoblarla en el cliente.

Cualquier caso de uso donde pueda existir la necesidad de invalidar el token a futuro queda por fuera de las posibilidades de la especificación y requiere una implementación propia lo que trae aparejado trabajo y problemas adicionales. Nuevamente, puede ser conveniente, utilizar sesiones y no tokens.

Pros y cons

Si bien se ya se pudieron observar más arriba, de forma sistematizada, los beneficios de utilizar JWT son, principalmente:

  • Tamaño reducido (comparado con SAML), como siempre, menos es mejor en lo que respecta a la cantidad de información que hay que trasmitir.
  • Estandarización / JSON, pocos formatos de datos son tan ampliamente soportados en la actualidad.
  • Separación servicios de autenticación de la app, no es necesario que el servicio de autenticación conviva con el la app o servicios a los que sirve.
  • Asegura integridad de los datos por especificación. La firma nos permite estar seguro que la información que recibimos es la enviada por emisor.
  • Puede servir para identificar el origen. Mediante la utilización de algorítmos asimétricos nos permite saber quién emitió el token.
  • Al no estar encriptado se simplifica la depuración en desarrollo.
  • Son tremendamente populares, podemos encontrar documentación, artículos e implementaciones de casi cualquier caso de uso o problema.

Con respecto a las contras de utilizar JWT se puede observar que:

  • Si bien es más chico que SAML, todavía puede resultar demasiado verboso para algunas aplicaciones más que nada dependiendo del payload.
  • Una vez emitido el token no se puede invalidar.
  • Al ser el token el que identifica al cliente y no el servidor el que individualiza las conexiones perdemos la posiblidad de generar notificaciones push.
  • La especificación no establece un algorítmo de firma fijo sino que la selección del mismo queda a cargo del mismo token lo que abre la puerta a diferentes vulnerabilidades23.
  • El uso de una firma puede generar una falsa sensación de seguridad con respecto a la información trasmitida.
  • Como corolario de las dos anteriores, es necesario tener conocimientos de crypto para saber los trade-offs de utilizar uno u otro algorítmo.
  • Son tremendamente populares, podemos encontrar un montón de información errónea validada por otro montón de fuentes igual de erróneas.

Seguridad

Como se mencionó antes, JWT tiene algunos problemas de seguridad más que nada derivados de la falta de especificidad de la especificación.

Como JWT no tiene definido un sistema de renovación de tokens y pedirle al usuario que se re autentique constantemente lleva a tener una carrera laboral muy corta, es común que los token se emitan con un TTL muy grande o incluso sin fecha de expiración.

El problema surge en que por especificación, un token que no está vencido o no tiene fecha de expiración es válido y mientras lo sea se puede reutilizar tantas veces como se quiera.

Sumado a que tampoco existen mecanismos para invalidar los tokens, un token comprometido es algo que, en principio, no hay forma de manejar.

Por último, otro problema muy extendido es la falta de seguridad al almacenar el token. Es muy común ver la recomendación de utilizar el LocalStorage de los navegadores como lugar para guardar los JWT sin considerar lo que eso implica.

Documentación extra

Fuentes