Un JWT son tres bloques de texto en base64url unidos por puntos. El primero es un header (encabezado) que nombra el algoritmo que firmó el token, el segundo es el payload (la carga útil) — un objeto JSON plano de claims, como quién es el usuario y cuándo deja de ser válido el token — y el tercero es una firma sobre los dos primeros. Solo la firma necesita una clave. El header y el payload están codificados, no cifrados, así que cualquiera que tenga el token puede leer todos los claims que hay dentro, incluida la persona a la que se lo emitieron.
Esa última frase es la que la gente entiende mal. Un JWT guardado en el local storage del navegador es legible por el usuario, por cualquier script de la página y por cualquiera que lo consiga después. Codificar no es poner un candado.
¿Cuáles son las tres partes de un JWT?
El header
Normalmente dos campos. alg nombra el algoritmo de firma — HS256, RS256 y ES256 cubren casi todo lo que vas a encontrar por ahí — y typ es casi siempre el string JWT. Los tokens de un proveedor de identidad suelen agregar kid, un identificador de clave que le dice al verificador cuál de las varias claves públicas del emisor tiene que usar.
La codificación es base64url: base64 común con - y _ en lugar de + y /, y sin el relleno final de =, para que el token se pueda meter en una URL o en un header sin escaparlo. Si ese esquema no te resulta familiar, base64 y para qué sirve en realidad es la lectura corta que conviene hacer antes.
El payload
Un objeto JSON cuyas claves se llaman claims. Algunos están estandarizados, la mayoría son lo que el emisor decidió poner ahí: un ID de usuario, una dirección de correo, un rol, un tenant, una lista de scopes.
No necesitas una clave ni una biblioteca para mirar. Pega el token en el decodificador de JWT de aquí y el header, el payload y cada claim salen en una tabla, con los timestamps convertidos en fechas reales. Funciona entero en el navegador y no hace ninguna petición de red, que es la única forma sensata de mirar un token que todavía está vivo.
La firma
Bytes en crudo, codificados en base64url, calculados sobre el string ASCII exacto header.payload — las formas codificadas, no el JSON. Cambia un carácter del payload y la firma ya no coincide.
Su longitud acota el algoritmo antes de que leas el header. HS256 produce 32 bytes, que salen como 43 caracteres. ES256 produce 64 bytes, o sea 86 caracteres. RS256 con una clave de 2048 bits produce 256 bytes, o 342 caracteres — por eso los tokens RS256 son mucho más voluminosos que los HS256 que llevan claims idénticos. Aun así, la longitud sola no es una prueba: HS512 también cae en 86 caracteres, así que HS512 y ES256 se ven idénticos desde afuera.
¿Qué claims son estándar?
El RFC 7519 registra siete, y todos y cada uno son opcionales. Un token sin ninguno de ellos sigue siendo un JWT válido.
iss— issuer, el emisor. Quién acuñó el token, normalmente una URL.sub— subject, el sujeto. Sobre quién o sobre qué trata el token, típicamente un ID de usuario.aud— audience, la audiencia. Qué aplicación debe aceptarlo.exp— hora de expiración.nbf— not before. El token no es válido hasta ese momento.iat— issued at, cuándo se emitió.jti— un ID único del token, usado para detectar replay.
Todo lo demás es invención del propio emisor. OpenID Connect agrega un conjunto conocido — email, name, nonce, auth_time — y tu propio backend seguramente agrega dos o tres más. No hay ningún esquema contra el que validar, así que un decodificador solo puede etiquetar los claims que reconoce; un claim propio sale con su valor en crudo y sin explicación, porque no hay dónde buscarla.
¿Por qué los timestamps parecen un disparate?
exp, nbf e iat son valores NumericDate: segundos desde el 1 de enero de 1970 UTC. Segundos, no milisegundos. Los timestamps Unix y cómo leer uno cubre el formato, pero la trampa específica de los JWT vale la pena decirla con todas las letras.
El Date.now() de JavaScript devuelve milisegundos. Ponlo tal cual en exp y acuñas un token que dice expirar dentro de decenas de miles de años — la cuenta de milisegundos de hoy, leída como segundos, cae en algún punto más allá del año 58.000. Todos los verificadores lo aceptan, nada registra una advertencia, y la duración de tu sesión pasa a ser, en la práctica, infinita. El número es mil veces demasiado grande y a simple vista parece perfectamente plausible: diez dígitos son una fecha de esta década, trece dígitos son un bug.
La otra cosa que hay que saber sobre estos campos es el desfase de reloj. Los relojes de los servidores se desvían, así que a los verificadores se les puede configurar una pequeña tolerancia en ambas direcciones, y 60 segundos es un ajuste común. Varias bibliotecas populares vienen sin ninguna tolerancia por defecto, así que revisa la tuya en lugar de suponer: un token que tu máquina considera expirado puede ser aceptado igual, y uno que parece fresco puede ser rechazado.
¿Qué demuestra en realidad una firma válida?
Una sola cosa: que el header y el payload no fueron alterados desde que alguien con la clave los firmó. Eso es genuinamente útil y también es toda la garantía.
No demuestra quién está presentando el token. Un JWT es una credencial al portador — quien lo tenga puede usarlo. Un token robado es un token válido, y la verificación de la firma va a pasar sin inmutarse para el ladrón.
Tampoco demuestra que el token fuera para ti. Verificar la firma no es lo mismo que aceptar el token: todavía tienes que confirmar que iss es el emisor en el que confías y que aud es tu aplicación. Un token correctamente firmado pero emitido para otro servicio sigue siendo el token equivocado, y un montón de integraciones se saltaron esa comprobación.
El campo alg es un claim, no un hecho
El header lo suministra quien envió el token, lo que significa que el atacante también lo controla. Dos ataques muy conocidos vienen de confiar en él. El primero es alg: none: las especificaciones JOSE incluyen un modo sin protección cuya firma es el string vacío, y una biblioteca que lee el algoritmo del header aceptará un token que un atacante escribió a mano. El segundo es degradar RS256 a HS256 — si el servidor verifica lo que sea que pida el header, un atacante vuelve a firmar el token con HS256 usando la propia clave pública del servidor como secreto HMAC. La clave es pública, así que el ataque sale gratis.
El arreglo es el mismo en todas las bibliotecas: fija el algoritmo que esperas en vez de leerlo del token.
¿Por qué nadie puede cancelar un token?
Un JWT es autocontenido por diseño. El servidor comprueba la firma y los timestamps y no toca ninguna base de datos, que es todo el argumento de rendimiento para usar uno. El costo es que cerrarle la sesión a alguien, quitarle un rol o banear una cuenta no puede surtir efecto hasta que el token expire, porque no se consulta nada en el momento de la verificación.
Cada solución alternativa renuncia a parte del beneficio. Una expiración corta más un refresh token mueve la consulta a la base de datos al paso de refresco. Una lista de jti revocados vuelve a poner la consulta justo donde la estabas evitando. Un claim con la versión del token comparado contra el registro del usuario hace lo mismo con menos filas. No hay ninguna combinación que conserve la ausencia de estado y consiga revocación instantánea.
¿Qué no debería ir nunca en el payload de un JWT?
Nada que no pegarías en un chat público. Los IDs internos de usuario, las direcciones de correo y los flags de rol se filtran por los tokens constantemente, porque quien los agrega está pensando en el payload como datos del servidor.
El tamaño también importa más de lo que la gente espera. El token viaja en cada petición, base64 agrega alrededor de un tercio a lo que sea que codifiques, y una cookie de navegador está limitada a unos 4 KB. Mete un perfil de usuario completo y una lista de permisos en el payload y tarde o temprano te vas a encontrar con un proxy que rechaza el header de la petición en lugar de un mensaje de error claro.
¿Qué no te puede decir un decodificador?
Leer un token localmente responde la mayoría de las preguntas, pero ten claro qué no puede hacer. Un decodificador que corre en el navegador no va a ir a buscar las claves del emisor: un verificador de verdad lee kid y trae la clave correspondiente desde un endpoint JWKS por la red, así que aquí tienes que pegar tú mismo el secreto o la clave pública. Tampoco tiene opinión sobre si iss y aud son los valores que tu aplicación debería aceptar — esa comprobación solo existe en tu código.
Los tokens cifrados son otro objeto completamente distinto. Un JWE tiene cinco partes en lugar de tres y su payload es texto cifrado, así que no hay nada que leer sin la clave de descifrado.
Y un detalle práctico con los secretos HMAC: algunos emisores guardan el secreto codificado en base64 y lo decodifican antes de firmar. Si un secreto que debería funcionar no verifica en ningún lado, el problema son los bytes que se están hasheando, no el token — decodifica el secreto primero, que en el decodificador de arriba es la casilla "El secreto está en base64".
Si tienes un token delante ahora mismo, el decodificador de JWT va a desplegar su header, su payload y sus claims, convertir los timestamps, marcar un token expirado o sin firmar, y comprobar la firma contra un secreto o una clave pública que pegues. No se envía nada a ninguna parte, aunque un token vivo en tu portapapeles ya es menos privado que uno que se quedó dentro de un header HTTP.
El claim con el que la gente tropieza más seguido es exp, y en realidad es un problema de timestamps Unix más que de JWT. Qué es un timestamp Unix y cómo leer uno explica por qué diez dígitos y trece dígitos significan fechas salvajemente distintas, que es la diferencia entre una sesión de una hora y una que no termina nunca.
Preguntas frecuentes
¿Qué información se guarda en un token JWT?
Un header que nombra el algoritmo de firma, un payload de claims y una firma. Los claims suelen incluir un ID de usuario, un emisor, una audiencia y los timestamps de expiración y de emisión, más lo que sea que el emisor haya agregado, como una dirección de correo, un rol o una lista de scopes. No hay un esquema fijo, así que dos tokens de servicios distintos pueden no parecerse en nada.
¿Cualquiera puede leer un JWT sin el secreto?
Sí. El header y el payload están codificados en base64url, que es un formato de texto que cualquiera revierte en un segundo. El secreto o la clave pública solo hacen falta para confirmar que el contenido no cambió desde que se firmó, no para leerlo.
¿Un JWT está cifrado?
Un JWT firmado estándar no está cifrado, solo codificado y firmado. Existe un formato aparte, JWE, cuyo payload sí es texto cifrado de verdad; se distinguen contando los puntos, porque un JWE tiene cinco partes en lugar de tres. Si un valor tiene que quedar en secreto, no va dentro de un token firmado.
¿Qué demuestra la firma de un JWT?
Solo que el header y el payload no fueron alterados desde que alguien con la clave los firmó. No dice nada sobre quién está presentando el token, así que un JWT robado pasa la verificación exactamente igual que uno legítimo. Tampoco confirma que el emisor y la audiencia sean los que tu aplicación espera, que es una comprobación aparte que tienes que hacer tú.
¿Cuánto tiempo debería ser válido un JWT?
Poco, porque un JWT no se puede cancelar antes de que expire. A los access tokens se les suele dar vidas de unos pocos minutos hasta una hora, con un refresh token de vida más larga encargándose de la renovación, para que la comprobación de revocación ocurra ahí. El número correcto es un equilibrio entre cada cuánto quieres ir a la base de datos y cuánto tiempo sigue siendo útil un token robado.
¿Por qué mi JWT nunca expira?
La causa habitual es poner milisegundos en el claim exp. Los timestamps de un JWT se cuentan en segundos desde el 1 de enero de 1970, así que un valor de trece dígitos en milisegundos se lee como una fecha a decenas de miles de años de distancia. Un exp de diez dígitos es una fecha de esta década; uno de trece dígitos es un bug.
Última actualización 19 de septiembre de 2026