Saltar al contenido

Buenas prácticas y errores comunes en JWT

Los JWT son engañosamente simples: así evitas las vulnerabilidades, las trampas de rendimiento y los errores de arquitectura más habituales.

4 min de lectura
Estructura de un token JWT que muestra las secciones de encabezado, payload y firma

Los JSON Web Tokens están en todas partes: autenticación, autorización de APIs, gestión de sesiones. Su simplicidad resulta atractiva: un payload autocontenido y firmado que codifica información del usuario. Pero esa simplicidad esconde un campo minado de vulnerabilidades de seguridad y decisiones de arquitectura que la mayoría de los tutoriales pasan por alto.

Qué es realmente un JWT

Un JWT son tres objetos JSON codificados en base64url y separados por puntos: encabezado, payload y firma.

tstypescript
// Header: algorithm and token type
{ "alg": "RS256", "typ": "JWT" }
 
// Payload: claims (data)
{
  "sub": "user_123",
  "name": "Alice",
  "role": "admin",
  "iat": 1587654321,
  "exp": 1587657921
}
 
// Signature: cryptographic proof that the header and payload are unmodified
// RSASHA256(base64url(header) + "." + base64url(payload), privateKey)

La firma garantiza la integridad — si alguien modifica el payload, la firma deja de coincidir. Pero no cifra nada. Cualquiera que tenga el token puede leer el payload.

Ataques de confusión de algoritmo

La vulnerabilidad más peligrosa de JWT es la manipulación del encabezado alg. Algunas bibliotecas confían en el algoritmo indicado dentro del propio token en lugar de imponer uno desde el servidor.

tstypescript
// ❌ Trusting the algorithm from the token header
const payload = jwt.verify(token, publicKey);
// An attacker can set alg: "none" and skip the signature entirely
// Or set alg: "HS256" and sign with the public key as the HMAC secret
 
// ✅ Always specify the expected algorithm
const payload = jwt.verify(token, publicKey, {
  algorithms: ["RS256"], // Only accept RS256
});

Restringe siempre los algoritmos aceptados. Nunca permitas "none". Si usas firma asimétrica (RS256), nunca aceptes firma simétrica (HS256).

Firma simétrica frente a asimétrica

AlgoritmoFirmaVerificaciónCaso de uso
HS256Secreto compartidoEl mismo secreto compartidoUn solo servicio
RS256Clave privadaClave públicaMicroservicios, verificación por terceros
ES256Clave privadaClave públicaIgual que RS256, con firmas más pequeñas
tstypescript
// HS256: same secret signs and verifies
// ❌ Every service that verifies tokens knows the signing secret
const token = jwt.sign(payload, "shared-secret", { algorithm: "HS256" });
jwt.verify(token, "shared-secret", { algorithms: ["HS256"] });
 
// RS256: only the auth service has the private key
// ✅ Other services verify with the public key — can't forge tokens
const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
jwt.verify(token, publicKey, { algorithms: ["RS256"] });

Usa RS256 o ES256 cuando varios servicios necesiten verificar tokens. Con HS256, cualquier servicio que pueda verificar también puede falsificar tokens.

Mantén los payloads pequeños

Los JWT se envían en cada solicitud, ya sea en encabezados, cookies o parámetros de URL. Un payload sobrecargado desperdicia ancho de banda y puede superar los límites de tamaño de los encabezados.

tstypescript
// ❌ Stuffing everything into the JWT
const token = jwt.sign({
  sub: "user_123",
  name: "Alice Johnson",
  email: "alice@example.com",
  role: "admin",
  permissions: ["read", "write", "delete", "manage-users", "view-analytics"],
  organization: { id: "org_456", name: "Acme Corp", plan: "enterprise" },
  preferences: { theme: "dark", language: "en", timezone: "UTC" },
}, key);
// This token is 500+ bytes — sent with EVERY request
 
// ✅ Minimal payload — look up details when needed
const token = jwt.sign({
  sub: "user_123",
  role: "admin",
  org: "org_456",
}, key, { expiresIn: "15m" });
// ~200 bytes — essential claims only

Incluye el ID del usuario, el rol y cualquier dato necesario para tomar decisiones de autorización. Todo lo demás puede consultarse en una base de datos o una caché cuando haga falta.

Expiración y revocación

Los JWT son sin estado — una vez emitidos, el servidor no puede revocarlos sin infraestructura adicional. Esta es la disyuntiva fundamental.

tstypescript
// Short expiration limits the damage window
const accessToken = jwt.sign(
  { sub: userId, role: user.role },
  privateKey,
  {
    algorithm: "RS256",
    expiresIn: "15m", // Short-lived
    issuer: "https://auth.myapp.com",
    audience: "https://api.myapp.com",
  },
);
tstypescript
// For immediate revocation, you need a blocklist
const revokedTokens = new Set<string>();
 
function verifyToken(token: string) {
  const payload = jwt.verify(token, publicKey, {
    algorithms: ["RS256"],
    issuer: "https://auth.myapp.com",
    audience: "https://api.myapp.com",
  });
 
  // Check if this specific token has been revoked
  if (revokedTokens.has(payload.jti)) {
    throw new Error("Token revoked");
  }
 
  return payload;
}

Una lista de bloqueo es una solución de compromiso — se pierde la ausencia de estado pura, pero se gana la capacidad de revocar. Guarda la lista de bloqueo en Redis con un TTL que coincida con la vida máxima del token, para que las entradas expiren solas.

Valida todo

tstypescript
// ❌ Minimal verification — many attack vectors remain
const payload = jwt.verify(token, key);
 
// ✅ Full verification — close every loophole
const payload = jwt.verify(token, publicKey, {
  algorithms: ["RS256"],      // Prevent algorithm confusion
  issuer: "https://auth.myapp.com",  // Reject tokens from other issuers
  audience: "https://api.myapp.com", // Reject tokens meant for other services
  clockTolerance: 30,         // Allow 30s clock skew
  maxAge: "1h",               // Reject tokens older than 1 hour
});
 
// Additional checks
if (!payload.sub) throw new Error("Missing subject claim");
if (!payload.role) throw new Error("Missing role claim");

Cuándo no usar JWT

Los JWT no siempre son la mejor opción. Si tu aplicación corre en un solo servidor y no necesita verificar tokens entre distintos servicios, las sesiones del lado del servidor son más simples y más seguras.

EscenarioJWTSesión del lado del servidor
Un solo servidorComplejidad innecesaria✅ Más simple
Microservicios✅ No requiere almacén de sesiones compartidoRequiere un almacén compartido
Necesita revocación instantáneaRequiere lista de bloqueo✅ Basta con eliminar la sesión
Autenticación entre dominios✅ AutocontenidoRequiere compartir cookies

Puntos clave

  1. Especifica siempre el algoritmo al verificar — nunca confíes en el encabezado alg del token
  2. Usa RS256/ES256 en arquitecturas con varios servicios — con HS256, cualquier verificador puede falsificar tokens
  3. Mantén los payloads mínimos — solo el ID del usuario, el rol y los datos imprescindibles para la autorización
  4. Una expiración corta (15 min) más tokens de refresco limita la ventana de daño si roban un token
  5. Los JWT no se pueden revocar sin una lista de bloqueo — esa es la disyuntiva fundamental de su naturaleza sin estado
  6. Valida el emisor, la audiencia y el algoritmo en cada verificación — no solo la firma
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX