Un JWT (JSON Web Token) son tres secciones codificadas en Base64url unidas por puntos. Decodificar uno para leer sus claims requiere una sola llamada a función y ninguna clave secreta —— por eso mismo el payload de un JWT no es un sitio seguro para datos sensibles. Esta guía explica qué es un JWT, cómo decodificar cada parte y la diferencia crítica entre decodificar y verificar un token.
¿Qué es un JWT?
Un JWT es un token compacto y URL-safe usado para llevar claims —— típicamente la identidad y permisos de un usuario —— entre partes. Está estandarizado como RFC 7519. Un token tiene este aspecto:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkLos dos puntos lo dividen en tres partes: header, payload y signature.
Las tres partes
- Header —— un pequeño objeto JSON que describe el algoritmo de firma, p. ej.
{"alg":"HS256","typ":"JWT"}. - Payload —— un objeto JSON con claims como
sub(subject),exp(expiración) y cualquier campo personalizado. - Signature —— una firma criptográfica sobre header y payload, usada para detectar manipulación. No es legible por humanos y no es JSON.
Header y payload son JSON codificados en Base64url. La firma se produce con JWS (RFC 7515).
Cómo decodificar un JWT en JavaScript
Divide por los puntos y decodifica Base64url las dos primeras partes. Ten en cuenta que JWT usa Base64url (con - y _ en vez de + y /, y sin padding), así que convierte antes de decodificar:
function decodeJwt(token) {
const [headerB64, payloadB64] = token.split('.');
const decode = (b64url) => {
const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
const json = atob(b64);
return JSON.parse(json);
};
return { header: decode(headerB64), payload: decode(payloadB64) };
} Para una seguridad Unicode completa, decodifica Base64 a bytes y pásalos por TextDecoder en vez de usar solo atob.
Cómo decodificar un JWT en Python
import base64, json
def decode_jwt(token):
header_b64, payload_b64, _sig = token.split('.')
def decode(part):
padded = part + '=' * (-len(part) % 4)
return json.loads(base64.urlsafe_b64decode(padded))
return decode(header_b64), decode(payload_b64)Decodificar no es verificar
Este es el punto más importante sobre los JWT. Cualquiera puede decodificar un token y leer su payload —— no se necesita clave. Decodificar te dice qué afirma el token; no te dice si esas claims son de fiar.
Para confiar en un token debes verificar la firma contra la clave secreta o pública del servidor, usando una librería revisada —— nunca implementes la tuya:
import jwt from 'jsonwebtoken';
// Verifica firma Y comprueba expiración; lanza si es inválido
const claims = jwt.verify(token, process.env.JWT_SECRET);
// jwt.decode(token) —— NO verifica; solo para inspecciónLa verificación en el servidor es lo que hace que un JWT sea credencial. Tratar un payload decodificado como confiable sin verificar la firma es un bug clásico de autenticación.
El payload de un JWT no está cifrado
Como el payload es solo JSON codificado en Base64url, ofrece cero confidencialidad. Es el mismo malentendido tratado en Base64 no es cifrado: la codificación la puede revertir cualquiera.
Nunca pongas contraseñas, números completos de tarjeta o datos personales sensibles en un payload JWT. Si de verdad necesitas un token cifrado, usa JWE (JSON Web Encryption, RFC 7516) —— un estándar separado y bastante menos común. Y evita pegar tokens reales en decodificadores online no fiables; ver por qué no deberías pegar JSON sensible en herramientas online.
Orden de validación: firma primero, claims después
Cuando verificas un token en producción, el orden importa. Un checklist seguro:
- Lista blanca de algoritmos —— rechaza cualquiera fuera de tu conjunto esperado (p. ej.
['RS256']). Nunca aceptesnone. - Firma —— verifica con la clave correcta (búsquela por
kiden tu JWKS). exp—— el token no debe estar expirado.nbf(not-before) —— no se debe usar demasiado pronto.iat—— sanity-check de que la emisión no es absurdamente futura (descontando deriva del reloj).iss—— el emisor debe coincidir con la autoridad de confianza que esperas.aud—— el audience debe incluir tu servicio.
La mayoría de librerías JWT hacen 1–4 por defecto y requieren opt-in para 6–7. Hasta que iss y aud se comprueben, un token válido para otro servicio de tu ecosistema podría reproducirse contra el tuyo.
Claims estándar comunes
| Claim | Significado |
|---|---|
iss | Issuer —— quién creó el token |
sub | Subject —— de quién trata el token (a menudo un ID de usuario) |
aud | Audience —— a quién va dirigido el token |
exp | Expiración —— timestamp Unix tras el cual el token es inválido |
iat | Issued-at —— timestamp Unix de creación |
nbf | Not-before —— el token no es válido hasta este momento |
La cabecera kid y la trampa alg: none
Dos campos de cabecera aparecen tan a menudo en JWT reales que merece la pena reconocerlos a primera vista:
kid(key ID) —— una pista que indica al verificador qué clave de un set (JWKS) firmó este token. Va en el header para que el verificador pueda elegir la clave correcta antes de comprobar la firma. Tratakidcomo entrada no confiable: búscalo en una lista blanca fija (o un JWKS descargado), nunca lo uses como ruta de fichero, parámetro SQL o URL.alg: "none"—— un algoritmo legal pero históricamente peligroso que significa "sin firma." Los decodificadores lo decodifican alegremente y un verificador permisivo puede aceptarlo, convirtiendo cualquier payload forjado en token "válido". Rechazaalg: noneexplícitamente y configura tu librería con el(los) algoritmo(s) exacto(s) que esperas (p. ej.['RS256']) en vez de fiarte de lo que el propio header del token afirme.
Decodifica un JWT en tu navegador
Para inspeccionar rápidamente los claims de un token, pega cualquiera de las secciones Base64url en el decodificador Base64 de fixjson —— decodifica al JSON subyacente localmente, sin enviar nada a un servidor. Para tokens no sensibles puedes usar también un depurador dedicado, pero para cualquier cosa con datos reales de usuario, una herramienta local-first es la opción segura.
Preguntas frecuentes
¿Cómo decodifico un JWT?
Divide el token por sus puntos y decodifica Base64url las dos primeras partes (header y payload) a JSON. La tercera es la firma y no es legible por humanos. No hace falta clave para decodificar.
¿Decodificar un JWT es lo mismo que verificarlo?
No. Decodificar solo lee los claims; cualquiera puede hacerlo. Verificar comprueba la firma contra una clave secreta o pública para confirmar que el token es auténtico y no se ha manipulado. Verifica siempre en el servidor antes de confiar en un token.
¿Cualquiera puede leer los datos de un JWT?
Sí. El payload está codificado en Base64url, no cifrado —— es texto plano para cualquiera que tenga el token. Nunca guardes secretos en un payload JWT. Ver Base64 no es cifrado.
¿Qué diferencia hay entre JWT, JWS y JWE?
Un JWT firmado usa JWS (RFC 7515) —— payload legible, evidencia de manipulación. JWE (RFC 7516) sí cifra el payload para confidencialidad. Los JWT comunes que ves en el día a día son JWS.
¿Para qué se usa el claim kid?
kid es un campo de cabecera (no un claim del payload) que identifica qué clave del JWKS firmó el token, para que el verificador escoja la correcta. Búscalo contra un set de claves fijo que tú controles —— no lo confíes como ruta o URL.
¿Por qué es peligroso alg: "none"?
Significa "sin firma." Un verificador permisivo que respete el algoritmo declarado por el propio token aceptará cualquier payload forjado. Configura siempre tu librería con el algoritmo exacto que esperas (p. ej. ['RS256']) y rechaza explícitamente none.
Inspecciona tokens localmente
- Codificador y decodificador Base64 —— decodifica las secciones header/payload de un JWT en tu navegador
- Base64 no es cifrado —— por qué cualquiera puede leer un payload JWT
- Por qué no pegar JSON sensible online —— manejar tokens reales de forma segura
- RFC 7519: JSON Web Token —— el estándar en sí