← Todos los artículos

Cómo decodificar un JWT y leer sus claims

Un JWT son tres secciones Base64url. Aprende a decodificar el header y el payload en JavaScript y Python — y por qué decodificar un token no es lo mismo que verificarlo.

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_wW1gFWFOEjXk

Los 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ón

La 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:

  1. Lista blanca de algoritmos —— rechaza cualquiera fuera de tu conjunto esperado (p. ej. ['RS256']). Nunca aceptes none.
  2. Firma —— verifica con la clave correcta (búsquela por kid en tu JWKS).
  3. exp —— el token no debe estar expirado.
  4. nbf (not-before) —— no se debe usar demasiado pronto.
  5. iat —— sanity-check de que la emisión no es absurdamente futura (descontando deriva del reloj).
  6. iss —— el emisor debe coincidir con la autoridad de confianza que esperas.
  7. 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

ClaimSignificado
issIssuer —— quién creó el token
subSubject —— de quién trata el token (a menudo un ID de usuario)
audAudience —— a quién va dirigido el token
expExpiración —— timestamp Unix tras el cual el token es inválido
iatIssued-at —— timestamp Unix de creación
nbfNot-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. Trata kid como 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". Rechaza alg: none explí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