Decodificar JWT en Node.js: jose vs jsonwebtoken vs a mano
Un JWT en Node.js son tres trozos Base64URL separados por puntos, según RFC 7519. Decodificar el payload es trivial. Verificar la firma es lo que para a un atacante, y la librería que elijas decide si lo haces bien.
Decodificar sin verificar es un footgun
La cabecera y el payload son públicos. Cualquiera puede partir un JWT por los puntos, decodificar Base64URL el segmento del medio y leer los claims. Eso es decodificar, no validar, y tratar esos claims como si fueran de fiar es como se cuelan los bugs de auth.
const [, payload] = token.split(".");
const claims = JSON.parse(
Buffer.from(payload, "base64url").toString("utf8"),
);
Usa esto exactamente una vez: en tests, o en un script de debug. En código de producción, el único camino seguro es verificar antes de leer. Si una función de tu flujo de auth llama a esto sin verificar primero, tienes una vulnerabilidad esperando al token adecuado.
jose es la elección moderna
jose (panva/jose) es el sucesor activo del espacio JWT en Node. Es ESM-first, soporta toda la suite JOSE (JWS, JWE, JWK, JWKS) y expone APIs tipadas. La verificación es asíncrona porque la importación de claves lo es:
```js import { jwtVerify, createRemoteJWKSet } from "jose";
const jwks = createRemoteJWKSet( new URL("https://example.com/.well-known/jwks.json"), );
const { payload } = await jwtVerify(token, jwks, { issuer: "https://example.com", audience: "api.example.com", }); ```
Las comprobaciones issuer y audience no son opcionales en ningún sistema en producción. Un token firmado por tu IdP pero emitido para otro audience sigue siendo válido criptográficamente; rechazarlo es trabajo tuyo, no de la librería.
jsonwebtoken aún sirve para HS256
jsonwebtoken es la elección histórica. Va con callbacks, se lleva bien con Buffer, y vale para tokens simétricos (HS256) con secret estático. Para verificación asimétrica o vía JWKS, la ergonomía se queda corta frente a jose.
```js const jwt = require("jsonwebtoken");
try { const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ["HS256"], issuer: "https://example.com", audience: "api.example.com", }); } catch (err) { // expirado, mal formado, mala firma, alg incorrecto } ```
El array algorithms es obligatorio. Si lo omites, reabres el bug de algorithm confusion de 2015, donde un token con alg: none o un alg cambiado pasa la verificación. Fija qué algoritmos aceptas; rechaza el resto.
Trampas típicas en Node
Buffer trata base64url como codificación real desde Node 16. El código antiguo que se montaba el alfabeto URL-safe a base de base64 y replace sobra: Buffer.from(seg, "base64url") es lo correcto.
El skew de reloj es el falso negativo número uno en producción. exp va en segundos, no milisegundos, y 30 segundos de deriva entre tu servidor y el IdP convierten tokens válidos en rechazos. Ambas librerías aceptan un clockTolerance; pon entre 30 y 60 segundos y olvídate.
Si decodificas un token en middleware y pasas los claims hacia abajo, fija req.user solo después de verificar con éxito. Mantener un req.unverifiedClaims vale para logs de debug, jamás para autorización.
Ejemplo completo
jsimport { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://example.com/.well-known/jwks.json"),
);
export async function authenticate(token) {
const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
issuer: "https://example.com",
audience: "api.example.com",
clockTolerance: 30,
});
return { sub: payload.sub, alg: protectedHeader.alg };
} ¿Solo necesitas el resultado?
Cuando solo quieres ver qué hay dentro de un JWT que has sacado de un curl o de la pestaña de red, abrir Node e importar jose sobra. Pégalo en el decodificador JWT del navegador y mira header, payload y bytes de firma separados, con el alg destacado, sin viajes de red.
Abrir Decodificador JWT →Preguntas frecuentes
¿jose o jsonwebtoken en 2026?
jose para código nuevo. Soporta rotación JWKS, ESM y toda la spec JOSE. jsonwebtoken vale para apps CommonJS legacy con HS256 y secret estático, pero está feature-frozen frente a jose.
¿Por qué falla un token verificado en producción?
Casi siempre skew de reloj o audience que no encaja. Pon clockTolerance a 30 segundos, loguea exp e iat al fallar y confirma que el audience coincide exactamente con el que emite el IdP.
¿Decodificar un JWT en cliente es seguro?
Decodificar el payload sí (los datos son públicos). Confiar en esos claims para cualquier decisión de autorización no. El navegador no puede verificar la firma contra una clave del servidor, así que cualquier decisión pasa por tu backend.