Anatomía de un JWT: cabecera, carga útil, firma, base64url y por qué decodificar no es verificar

Tres puntos: la estructura de un JSON Web Token

Un JSON Web Token es una cadena de exactamente tres valores codificados en base64url separados por puntos: base64url(cabecera).base64url(carga).base64url(firma). El RFC 7519, publicado en mayo de 2015, define el formato. Cualquier JWT que encuentres — ya sea en un encabezado Authorization, una cookie o un parámetro de consulta de URL — sigue esta estructura. Las tres partes pueden decodificarse de forma independiente, y la firma es lo que las une con garantías criptográficas.

El formato fue diseñado para ser compacto y seguro en URLs. Antes de JWT, la autenticación de API dependía de identificadores de sesión opacos almacenados en el servidor, lo que requería una búsqueda en base de datos en cada solicitud. Los JWT integran las afirmaciones directamente en el token, de modo que un servidor sin estado puede verificarlas sin consulta alguna. Esta comodidad conlleva responsabilidades: a diferencia de un token de sesión que el servidor puede revocar de inmediato, un JWT firmado permanece válido hasta que su claim exp expire, a menos que el servidor mantenga una lista de denegación.

Codificación base64url: la variante URL-segura de base64

El base64 estándar codifica datos binarios con 64 caracteres: A–Z, a–z, 0–9, + y /, con relleno =. Dos de esos caracteres (+ y /) tienen significado especial en URLs y cadenas de consulta HTTP. Base64url, definido en el RFC 4648, Sección 5, reemplaza + por - y / por _, y omite completamente el relleno =. El resultado puede usarse directamente en URLs, cabeceras HTTP y cookies sin codificación adicional.

La tasa de expansión es la misma que en base64 estándar: cada 3 bytes de entrada se convierten en 4 caracteres base64url — un factor de 4/3, aproximadamente un 33% de expansión. Como la codificación es reversible sin ninguna clave, cualquiera que obtenga un JWT puede leer su cabecera y carga decodificando — base64url es codificación, no cifrado.

La cabecera: selección de algoritmo y tipo de token

La cabecera es un objeto JSON que indica al receptor cómo procesar el token. Una cabecera mínima contiene dos campos: alg (el algoritmo de firma) y typ (siempre "JWT" para tokens estándar). El RFC 7518 define los identificadores de algoritmo. Los más comunes son: HS256 — HMAC-SHA256, un algoritmo simétrico donde se usa el mismo secreto para firmar y verificar; RS256 — RSA-PKCS1v1.5 con SHA-256, asimétrico, donde una clave privada firma y la clave pública correspondiente verifica; ES256 — ECDSA con P-256 y SHA-256, también asimétrico pero con firmas más cortas que RSA.

La elección entre algoritmos simétricos y asimétricos tiene consecuencias arquitectónicas reales. Con HS256, cada servicio que necesite verificar tokens debe tener el secreto compartido. Con RS256 o ES256, el emisor mantiene la clave privada en privado y publica solo la clave pública (a menudo a través de un endpoint JWKS), por lo que los servicios verificadores nunca manejan material de firma. Para integraciones multi-servicio o de terceros, los algoritmos asimétricos son preferibles.

La carga útil: claims registrados y su significado

La carga útil es un objeto JSON que contiene claims — afirmaciones sobre una entidad, normalmente el usuario autenticado, más metadatos sobre el propio token. La Sección 4.1 del RFC 7519 define siete nombres de claims registrados con semántica estandarizada: iss (emisor), sub (sujeto, a menudo un ID de usuario), aud (audiencia, los destinatarios previstos), exp (tiempo de expiración, un NumericDate tras el cual el token no debe aceptarse), nbf (no antes de, un NumericDate antes del cual el token no debe aceptarse), iat (emitido en) y jti (ID del JWT, un identificador único útil para prevenir ataques de repetición).

NumericDate es el número de segundos desde 1970-01-01T00:00:00Z UTC (el epoch Unix), no milisegundos. Esta es una fuente común de errores para los desarrolladores familiarizados con Date.now() de JavaScript. La carga útil de un JWT estándar está firmada pero no cifrada — cualquiera que intercepte el token puede leer todos los claims. Si la carga contiene datos sensibles, el token debe cifrarse con JWE (RFC 7516) o transmitirse solo a través de TLS.

La firma: qué protege — y el problema de alg:none

La firma se calcula sobre la entrada de firma, que es base64url(cabecera) + "." + base64url(carga). Para HS256 es HMAC-SHA256(secreto, entrada_firma) codificado en base64url. Dado que la entrada incluye tanto la cabecera como la carga, cualquier modificación en cualquiera de las partes — incluso cambiar un solo carácter — invalida la firma. Un servidor que verifica la firma correctamente puede estar seguro de que el token no fue manipulado.

En 2015 se descubrió una clase de vulnerabilidades en múltiples bibliotecas JWT: el ataque alg:none. El RFC 7519 técnicamente permite "alg":"none" para indicar un JWT no asegurado sin firma. El fallo: algunas bibliotecas leían el campo alg de la cabecera (no verificada) para seleccionar la ruta de verificación — si un atacante cambiaba la cabecera a "alg":"none" y eliminaba la firma, la biblioteca declaraba el token válido sin comprobar ninguna firma. CVE-2015-9235 documenta esto en la popular biblioteca node-jsonwebtoken. La solución: la parte verificadora debe especificar el algoritmo esperado explícitamente y rechazar cualquier token cuya cabecera nombre uno diferente.

Decodificar frente a verificar: por qué la distinción importa

Decodificar un JWT significa aplicar base64url-decode a las tres partes para recuperar el JSON de la cabecera, la carga y los bytes brutos de la firma. No se necesita ninguna clave. Cualquiera que tenga una cadena JWT puede decodificarla y leer los claims. Esto es por diseño — los JWT no son contenedores opacos. Un desarrollador depurando un problema de autenticación, un ingeniero leyendo un registro de acceso, un investigador de seguridad auditando un sistema: todos pueden decodificar cualquier JWT que tengan.

Verificar un JWT significa comprobar que la firma fue producida por la clave correcta sobre los bytes exactos de cabecera y carga. Una verificación exitosa prueba dos cosas: el token fue creado por el titular de la clave de firma (autenticidad), y ni la cabecera ni la carga han sido alteradas desde la firma (integridad). La verificación requiere la clave — el secreto compartido para HMAC, la clave pública para RSA/ECDSA.

El error crítico es tratar un JWT decodificado como confiable sin verificación. Un cliente que lee claims de un JWT en localStorage y actúa sobre ellos sin verificación de firma en el servidor está confiando en datos que cualquier usuario puede fabricar libremente. Un atacante puede crear {"sub":"admin","role":"superuser"}, codificarlo en base64url con cualquier cabecera y adjuntar una firma falsa o vacía — se decodificará perfectamente. Las decisiones de autorización siempre deben tomarse después de la verificación criptográfica en el servidor. Un decodificador JWT es la herramienta correcta para inspección y depuración; nunca sustituye la verificación del lado del servidor en una aplicación en producción.