El malentendido mas comun que escucho sobre JWT es: este token esta cifrado con JWT.
No esta cifrado. El header y el payload de un JWT estan codificados en Base64URL, la misma familia que Base64. Cualquier persona con la cadena puede pegarla en cualquier decodificador JWT en linea y leer el identificador de usuario, la fecha de expiracion y la lista de roles al instante. Este articulo existe para que la proxima vez que escuches cifrado con JWT puedas decir de inmediato: esta firmado, no cifrado.
Resumen en 30 segundos
- Un JWT (JSON Web Token) suele tener tres partes: header.payload.signature, separadas por puntos.
- El header y el payload estan codificados en Base64URL (RFC 4648). Son reversibles y completamente legibles.
- La signature es una firma digital que el servidor genera sobre las dos primeras partes usando un secreto o una clave privada. Demuestra que el token no se ha manipulado. No demuestra que el contenido sea confidencial.
- Nunca metas contrasenas, API keys u otros secretos en el payload. Para proteger un secreto usa cifrado real (AES, libsodium, age, GPG).
Como se ve un JWT real
La herramienta Decodificador JWT tiene un boton Load sample que produce un JWT estandar de tres partes. La herramienta muestra cada una de las tres partes decodificadas a la derecha, mas una nota de seguridad: signature not verified. Esa nota es el estado normal, porque la decodificacion por si sola nunca verifica la firma.
En concreto, el header decodificado contiene los campos alg y typ. alg es el algoritmo de firma, typ suele ser JWT. El payload decodificado contiene claims como iss, sub, aud, iat, exp, jti y role, que representan respectivamente el emisor, el sujeto, la audiencia, la fecha de emision, la fecha de expiracion, el id del token y el rol.
5 escenarios reales donde se usa JWT mal
Escenario 1: meter la contrasena del usuario en el payload
Ejemplo de payload: un campo como password: Hunter2 dentro del payload del JWT.
Por que la gente cae: asume que un token firmado por el servidor es automaticamente seguro. En la practica, cualquiera que obtenga el token (extension del navegador, log del cliente, header Referer, cache de CDN) puede pegarlo en jwt.io o en nuestro Decodificador JWT y leer el campo password en un segundo. La firma evita la manipulacion, no la lectura.
Solucion: las contrasenas nunca van en un JWT. Los campos sensibles van en un canal cifrado aparte (cifrado en el servidor, un vault, etc.) y solo los ids y roles no sensibles van en los claims.
Escenario 2: meter una API key o cadena de conexion en el payload
Ejemplo de payload: un campo como api_key: … dentro de los claims.
Meter una API key en el payload es la misma clase de error que meter una contrasena. Un atacante que roba el JWT de cualquier usuario puede leer la API key del backend directamente desde el payload y llamar a cualquier endpoint interno sin pasar por la autenticacion.
Solucion: las API keys, cadenas de conexion y tokens son secretos y deben vivir en el servidor. Si el cliente necesita referenciar uno, pasa por un endpoint de autenticacion dedicado que emita un bearer token de corta duracion.
Escenario 3: meter campos privados del usuario en el payload
Ejemplo de payload: campos como email, phone y ssn_last4 metidos en los claims.
JWT existe para pasar claims entre servicios, y la ruta intermedia (gateways, logs, CDNs) a menudo ve el token completo. Escribir campos privados en el payload equivale a difundir esos campos por todos tus sistemas y viola GDPR y leyes de privacidad similares.
Solucion: limita el JWT a la identidad minima necesaria: sub, role, exp, iss, aud. Deja que el receptor busque los campos privados en su propia base de datos usando sub.
Escenario 4: poner alg en none o no validar el algoritmo
Ejemplo de header: alg: none, o una signature vacia.
Este es el ataque de libro de texto. El atacante cambia alg a none, borra la signature y falsifica un payload que dice admin. Si el servidor no aplica el algoritmo de forma estricta, la solicitud pasa sin problemas. Nuestro Decodificador JWT muestra una advertencia en rojo cuando alg es none o la signature esta vacia: los sistemas de produccion normalmente deberian rechazarlo.
Solucion: el servidor debe usar una lista blanca estricta de algoritmos (por ejemplo, solo RS256 o HS256) y comparar el campo alg contra esa lista. Nunca elijas el algoritmo de verificacion dinamicamente segun el header. Ese es un patron clasico de CVE.
Escenario 5: asumir que exp sigue valido porque el token se decodifica
Ejemplo de payload: un campo como exp: 1700000000 (segundos) que la libreria trata como milisegundos, o un reloj del servidor que se ha desviado.
Si el reloj del servidor esta desincronizado, el reloj del cliente se ha desviado, o la unidad de iat/exp es incorrecta (milisegundos frente a segundos), puedes ver lo contrario de lo que esperas: un token cuyo exp aun no llega se rechaza, o uno cuyo exp ya paso se acepta.
Solucion: el servidor siempre usa su propio tiempo, nunca confia en headers del cliente. Si la herramienta muestra iat muy en el futuro, o exp parece un valor de 13 digitos de milisegundos en vez de 10 digitos de segundos, muestra una advertencia millisecondTimestamp. Por eso pasar un token por el Decodificador JWT antes de meterse en problemas de auth te ahorra horas.
4 datos contraintuitivos
Dato 1: una cadena JWT es un 30-50% mas larga que el JSON original
La codificacion Base64URL infla el tamano. Tres bytes se convierten en cuatro caracteres, y cuanto mayor sea el payload mayor sera el overhead. Para claims cortos es invisible, pero unos pocos KB de payload muestran la diferencia.
Dato 2: la signature no te dice que el contenido sea genuino
La firma demuestra una cosa: el servidor la firmo con su secreto, asi que cambiar un solo byte rompe la firma. No demuestra quien firmo, si el firmante es de confianza, o si la clave de firma se ha filtrado.
La verificacion es trabajo del servidor, no del cliente. Despues de que un cliente recibe un token, lo unico que puede hacer es devolverlo al servidor para que lo verifique. Por eso nuestra herramienta dice explicitamente que la signature no esta verificada.
Dato 3: las librerias de distintos lenguajes se comportan distinto por defecto
- jsonwebtoken (Node.js): acepta muchos algoritmos por defecto. En produccion debes pasar una lista blanca explicita como algorithms: [RS256].
- PyJWT (Python): estricto por defecto. Solo valida cuando pasas un argumento algorithms= explicito.
- java-jwt (Java): permisivo por defecto. Debes especificar algorithms a mano.
Cuando cruzas limites entre lenguajes, una mirada rapida al comportamiento por defecto de la otra libreria evita la mayoria de los CVE.
Dato 4: JWT expirado = rechazado por el servidor, no por el cliente
El claim exp es para el servidor. Aunque el cliente vea un token cuyo exp aun esta en el futuro, el servidor puede seguir rechazandolo. El servidor puede imponer una vida util mas corta, forzar un refresh o invalidar el token de forma activa segun otras politicas (cambio de IP, cambio de rol, revocacion manual).
Por eso un status que parece valido en el Decodificador JWT no garantiza que el servidor acepte el token.
Practicas recomendadas
- Limita el payload al minimo: sub, iss, aud, exp, iat, jti, role. Sin contrasenas, API keys, datos privados ni cadenas de conexion.
- Antes de verificar, codifica una lista blanca de algoritmos (por ejemplo algorithms: [RS256]) y nunca elijas dinamicamente desde el header.
- Deja que el servidor genere exp por si mismo, nunca confies en el exp del cliente. Un cliente que muta el payload y lo vuelve a firmar no lo consigue (la firma no cuadrara), pero no cuentes con eso.
- Cuando depures problemas de auth, usa el Decodificador JWT offline para leer los claims. Nunca pegues un token real en un sitio que lo suba. Nuestra herramienta se ejecuta 100% en local: nada sale del navegador y no se guardan logs.
- Para proteger un secreto, usa cifrado real. JWT no resuelve la confidencialidad, solo la autenticacion y la resistencia a la manipulacion. Para secretos usa AES-GCM, libsodium o age.
- JWT no es una sesion. Es sin estado, pero eso no significa que sea revocable. Para invalidar tokens, el servidor debe mantener una denylist, o usar vidas utiles cortas con refresh tokens.
JWT es firma mas codificacion, no cifrado. Cuando necesites confidencialidad, usa cifrado. Cuando necesites autenticacion, JWT es la herramienta adecuada.
Prueba nuestro Decodificador JWT: haz clic en Load sample para generar un JWT estandar y luego en Decode para ver los segmentos header, payload y signature, junto con la nota roja de signature-not-verified. Para comparar Base64 con cifrado real, lee nuestro articulo Base64 no es cifrado y la herramienta Codificador de texto.