Para decodificar un token JWT, divide la cadena por sus dos puntos en tres partes, luego decodifica en base64url el encabezado y el payload. El payload es JSON plano que cualquiera puede leer, porque un JWT está codificado, no cifrado. En JavaScript puedes hacerlo en una línea con atob, pero solo después de corregir los caracteres base64url que rompen una decodificación ingenua. Esta guía muestra exactamente cómo decodificar un token JWT manualmente, el detalle que la mayoría de los fragmentos omiten, y por qué decodificar no te dice nada sobre si el token es real.
Si alguna vez has pegado un token en algún lado y has visto aparecer los claims al instante, esa es la parte que confunde a la gente. No hay ninguna clave secreta involucrada en leer un JWT. La privacidad proviene de la firma, no de ocultar el contenido, y confundir esas dos ideas es como los desarrolladores introducen errores reales. Primero decodifiquemos uno correctamente, luego aclaremos el malentendido peligroso que subyace.
Cómo es realmente un JWT#
Un JSON Web Token es una sola cadena compuesta por tres partes unidas por puntos:
header.payload.signature
Un token real se ve así (con saltos de línea aquí para facilitar la lectura, pero es una cadena continua):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNzE3NzI4MDAwLCJleHAiOjE3MTc3MzE2MDB9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Cada parte tiene una función:
- Header: un pequeño objeto JSON que indica el algoritmo de firma (
alg) y el tipo de token (typ). Codificado en base64url. - Payload: el objeto JSON que contiene tus claims (id de usuario, roles, fecha de expiración, cualquier cosa que pongas). Codificado en base64url.
- Signature: un hash criptográfico del header y el payload, calculado con una clave secreta o privada. Es la única parte que demuestra que el token no ha sido manipulado.
El header y el payload no están ocultos. Están en base64url, que es reversible por cualquier persona sin clave. Trata todo lo que hay en un JWT payload como información pública.
Por qué base64url y no base64 estándar#
Los JWT se usan en URLs, cabeceras HTTP y cookies. El base64 estándar usa los caracteres +, / y el relleno =, que no son seguros o resultan molestos en esos contextos. Por eso los JWT usan base64url, definido en RFC 4648, que reemplaza + por -, / por _ y elimina el relleno =.
Esa única diferencia es la razón por la que la mayoría de los fragmentos de código para decodificar copiados y pegados fallan con tokens reales. Si decodificas en base64 un payload que contiene - o _ sin convertirlo primero, obtienes una salida distorsionada o un error. Nosotros lo manejamos explícitamente más abajo. Si quieres ver los mecanismos de codificación por separado, nuestro codificador y decodificador base64 te permite experimentar con salida estándar frente a salida segura para URL.
Cómo decodificar un token JWT en JavaScript#
El navegador te proporciona atob para la decodificación base64. El truco está en convertir primero base64url a base64 estándar y luego manejar correctamente Unicode. Aquí tienes la versión completa y correcta, no la línea única defectuosa que sueles encontrar.
Paso 1: Dividir el token por los puntos#
El token es header.payload.signature. Divídelo y toma las partes que necesites.
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIn0.signature_here";
const [headerB64, payloadB64, signatureB64] = token.split(".");
Si token.split(".") no devuelve exactamente tres partes, la cadena no es un JWT bien formado y deberías detenerte aquí en lugar de decodificar basura.
Paso 2: Convertir base64url de vuelta a base64 estándar#
Este es el paso que casi todos los fragmentos rápidos omiten, y es exactamente por lo que esos fragmentos fallan con tokens de producción que contienen - o _.
function base64UrlToBase64(input) {
// Reemplazar caracteres seguros para URL con caracteres base64 estándar
let output = input.replace(/-/g, "+").replace(/_/g, "/");
// Reagregar el relleno que base64url eliminó
const pad = output.length % 4;
if (pad) {
output += "=".repeat(4 - pad);
}
return output;
}
Sin restaurar el relleno, algunos entornos lanzan un InvalidCharacterError en atob. Con él, decodificas de forma fiable siempre.
Paso 3: Decodificar con atob y analizar el JSON#
Ahora atob funciona, pero hay un detalle más. atob devuelve una cadena binaria, por lo que cualquier carácter no ASCII (nombres acentuados, emojis, escrituras no latinas) sale distorsionado. Decodifica los bytes como UTF-8 para estar seguro.
function decodeJwtPart(part) {
const base64 = base64UrlToBase64(part);
const binary = atob(base64);
// Convertir la cadena binaria a una cadena UTF-8 adecuada
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
const json = new TextDecoder("utf-8").decode(bytes);
return JSON.parse(json);
}
const header = decodeJwtPart(headerB64);
const payload = decodeJwtPart(payloadB64);
console.log(header); // { alg: "HS256", typ: "JWT" }
console.log(payload); // { sub: "1234567890", name: "Jane Doe", ... }
Paso 4: Leer los claims que te interesan#
El payload ahora es un objeto JavaScript normal. Extrae los claims estándar:
console.log("Sujeto:", payload.sub);
console.log("Emitido en:", new Date(payload.iat * 1000).toISOString());
console.log("Expira en:", new Date(payload.exp * 1000).toISOString());
console.log("¿Expirado?", payload.exp * 1000 < Date.now());
Ten en cuenta que iat y exp son marcas de tiempo Unix en segundos, así que multiplícalos por 1000 antes de pasarlos a Date. Olvidar esto te dará marcas de tiempo en 1970 y una tarde muy confusa.
Nota sobre Node.js#
En Node, atob existe en versiones modernas, pero la forma idiomática de decodificar usa Buffer, que maneja base64url y UTF-8 de una sola vez:
const payload = JSON.parse(
Buffer.from(payloadB64, "base64url").toString("utf8")
);
El indicador de codificación "base64url" hace el intercambio de caracteres y el relleno por ti, por lo que el código de Node se ve más limpio que el del navegador para esta tarea.
Decodificar no es verificar: el error que causa las filtraciones#
Aquí está la parte que los fragmentos de código nunca advierten, y es lo más importante de esta página. Decodificar un JWT solo lee los reclamos. No confirma que esos reclamos sean verdaderos.
Cualquiera puede tomar un token, cambiar el payload (por ejemplo, cambiar "role": "user" a "role": "admin"), recodificarlo y devolverlo a tu servidor. El payload decodificado se verá perfectamente válido. Lo único que se interpone entre ese token falsificado y tus datos es la verificación de la firma, que requiere la clave secreta o pública y que la decodificación nunca toca.
| Acción | ¿Necesita una clave? | ¿Qué prueba? |
|---|---|---|
| Decodificar encabezado/payload | No | Lo que el token afirma decir |
| Verificar firma | Sí | Que el token fue emitido por ti y no ha sido modificado |
Verificar exp / nbf | No (pero solo después de verificar) | Que el token está dentro de su ventana de tiempo válida |
La regla es simple y absoluta:
Nunca confíes en un payload de JWT decodificado para tomar una decisión de seguridad. Decodifícalo solo para visualización y depuración. En el servidor, siempre verifica la firma con una biblioteca confiable antes de actuar sobre cualquier reclamo.
Un token decodificado es un reclamo, no un hecho. Tratar el payload como verdad absoluta porque "se ve bien" es exactamente cómo se introducen los errores de escalada de privilegios. Para obtener un análisis completo de por qué estas son dos operaciones diferentes y dónde se equivocan los equipos, lee nuestra guía sobre decodificar versus verificar en seguridad JWT.
"¿Está cifrado un JWT?" No, y eso importa#
Este es el concepto erróneo detrás de la mayoría de los errores con JWT. Un JWT firmado estándar (un JWS) está codificado, no cifrado. El payload es completamente legible para cualquiera que intercepte el token. La firma protege la integridad (nadie puede cambiarlo sin ser detectado), no la confidencialidad (cualquiera puede leerlo).
Las consecuencias prácticas:
- No pongas secretos en un payload de JWT. Sin contraseñas, sin claves API, sin datos personales que no imprimirías en una valla publicitaria. Está a una decodificación base64url de ser leído.
- Si realmente necesitas confidencialidad, necesitas JWE (JSON Web Encryption), un formato diferente y menos común que realmente cifra el payload.
- Asume que cada JWT que emitas será inspeccionado. Los registros, las herramientas de desarrollo del navegador, los proxies y cualquier decodificador pueden leerlo.
Lectura de las declaraciones estándar#
El payload puede contener cualquier cosa, pero un conjunto de declaraciones registradas tienen significados definidos. Conocerlos te ayuda a depurar problemas de autenticación rápidamente.
| Declaración | Nombre | Significado |
|---|---|---|
iss | Emisor | Quién creó y firmó el token |
sub | Sujeto | De quién trata el token, normalmente un id de usuario |
aud | Audiencia | Para quién está destinado el token |
exp | Expiración | Tiempo Unix (segundos) después del cual el token no es válido |
nbf | No antes | Tiempo Unix antes del cual el token no es válido |
iat | Emitido en | Tiempo Unix en que se creó el token |
jti | ID de JWT | Un identificador único para el token |
Cuando estés persiguiendo un error de "por qué este usuario está siendo desconectado", decodifica el token y verifica exp primero. Un exp vencido o un problema de desfase de reloj con nbf es el culpable más común, y puedes detectarlo en segundos una vez que el payload es legible.
Cuándo usar una herramienta decodificadora en su lugar#
Escribir la función de decodificación vale la pena una vez para entender cómo funciona. Sin embargo, para la depuración diaria, pegar una función en la consola para cada token es lento, y un mal copiado y pegado reintroduce el error de base64url que acaba de corregir.
Un decodificador basado en navegador es más rápido y seguro para la inspección:
- Divide, convierte y muestra de forma ordenada el encabezado y el payload al instante.
- Señala un
expvencido para que no tenga que hacer el cálculo de la marca de tiempo. - Se ejecuta en su navegador, por lo que el token no se envía a un servidor (importante, ya que el token es una credencial activa).
Ese último punto es más importante de lo que la gente cree. Un JWT suele ser un token de sesión activo. Pegarlo en un decodificador online sospechoso que lo envía a un backend es entregar credenciales funcionales. Nuestro decodificador JWT gratuito decodifica completamente en su navegador, muestra el encabezado, el payload y una fecha de vencimiento legible, y nunca transmite el token a ningún lado. Para el contexto de seguridad sobre lo que un decodificador puede y no puede decirle, nuestra guía de seguridad de tokens y decodificador JWT profundiza en hábitos de inspección seguros.
Cómo decodificar un token JWT de forma segura: el resumen#
Para decodificar un token JWT, divídelo por los puntos, convierte cada parte base64url a base64 estándar (intercambia - y _, restaura el relleno), ejecuta atob y analiza el JSON. En Node, Buffer.from(part, "base64url") hace la conversión por ti. El encabezado indica el algoritmo, el payload contiene las declaraciones, y iat y exp son segundos desde 1970.
Lo que no se puede negociar: decodificar es leer, no confiar. Un payload JWT es público, reversible y falsificable. Decodifícalo libremente para mostrar y depurar, pero toma cada decisión de autorización real en el servidor después de verificar la firma con una clave. Entiende bien esa diferencia y los JWT son simples. Si la difuminas, tendrás un agujero de seguridad que parece código funcional.
Preguntas Frecuentes#
¿Cómo decodificar un token JWT sin una librería?
Divide el token por sus puntos en header, payload y firma, luego decodifica el header y payload con base64url. En el navegador, convierte base64url a base64 (reemplaza - con +, _ con / y agrega el relleno =) antes de llamar a atob, luego JSON.parse el resultado. En Node, Buffer.from(part, "base64url").toString("utf8") lo hace en una línea.
¿Por qué falla mi llamada a atob en un JWT?
Porque los JWT usan base64url, no base64 estándar. Contienen - y _ en lugar de + y /, y omiten el relleno =. Pasar esa cadena sin procesar a atob arroja un error o devuelve basura. Primero convierte los caracteres y restaura el relleno, que es el paso que omiten la mayoría de los fragmentos rápidos.
¿Un JWT está cifrado o solo codificado? Un JWT firmado estándar está codificado, no cifrado. El payload está en base64url, cualquiera puede revertirlo sin clave, así que trata su contenido como público. La firma protege contra manipulaciones, no contra la lectura. Si necesitas que el payload sea confidencial, usa JWE (Cifrado JSON Web) en su lugar.
¿Alguien puede leer o cambiar mi payload JWT? Cualquiera que tenga el token puede leer el payload al instante, ya que solo es base64url. También pueden cambiarlo y recodificarlo. Lo que no pueden hacer es producir una firma válida sin tu clave secreta o privada, por lo que tu servidor debe verificar la firma en lugar de confiar en las afirmaciones decodificadas.
¿Decodificar un JWT verifica que sea válido?
No. Decodificar solo lee lo que el token afirma. La verificación es un paso aparte que comprueba la firma con tu clave para confirmar que el token es auténtico y no ha sido modificado, y luego verifica exp y nbf para los tiempos. Nunca tomes una decisión de autorización basada solo en un payload decodificado.
¿Qué significan iat y exp en un JWT?
Son marcas de tiempo. iat (emitido en) es cuándo se creó el token, y exp (expiración) es cuándo deja de ser válido. Ambos son tiempo Unix en segundos, así que multiplica por 1000 antes de construir un Date de JavaScript. Un token está expirado cuando exp * 1000 es menor que la hora actual.



