Mejores prácticas de seguridad JWT para 2026
· Cosyslabs
Las fallas de seguridad en JWT se encuentran consistentemente entre las principales vulnerabilidades de API. Use los algoritmos RS256 o ES256, rechace explícitamente el algoritmo none, configure el tiempo de expiración del token de acceso en menos de 15 minutos, implemente la rotación de tokens de actualización y siempre verifique las firmas en el servidor antes de confiar en cualquier claim.
¿Qué es un JWT?
Un JSON Web Token son tres segmentos codificados en Base64URL separados por puntos:
cabecera.payload.firma
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.firma
- Cabecera: algoritmo y tipo de token
- Payload: claims (datos del usuario, expiración, etc.)
- Firma: prueba criptográfica de que el token no fue alterado
La firma es lo que debes verificar. Un JWT sin firma verificada es simplemente JSON no verificado.
Vulnerabilidad crítica: Confusión de algoritmo
El ataque del algoritmo none
Las primeras bibliotecas JWT aceptaban alg: "none" en la cabecera, lo que significa que no se requería firma. Un atacante podía forjar cualquier payload:
{
"alg": "none",
"typ": "JWT"
}
Sin verificación de firma, podían afirmar ser cualquier usuario. Solución:
// Node.js — siempre especifica explícitamente los algoritmos permitidos
jwt.verify(token, clavePublica, { algorithms: ["RS256"] });
// Nunca permitir "none"
// Nunca usar algorithms: ["RS256", "none"] — esto es una vulnerabilidad
Ataque de confusión RS256 vs HS256
HS256 (HMAC) usa un secreto compartido — la misma clave firma y verifica. RS256 (RSA) usa un par de claves — la clave privada firma, la clave pública verifica.
El ataque: si una biblioteca ve alg: "HS256" y usa la clave pública RS256 como secreto HMAC, un atacante que obtuvo la clave pública (¡que es pública!) puede forjar tokens.
Siempre fija el algoritmo en el servidor. Nunca leas alg del token para decidir cómo verificarlo.
// Vulnerable — lee alg del token
function verificar(token, clave) {
const { alg } = decodificarCabecera(token);
return verificarCon(token, clave, alg); // NUNCA hagas esto
}
// Seguro — el algoritmo está codificado en el servidor
function verificar(token) {
return jwt.verify(token, CLAVE_PUBLICA, { algorithms: ["RS256"] });
}
Recomendaciones de algoritmos
| Algoritmo | Tipo | Caso de uso |
|---|---|---|
| ES256 | Asimétrico (ECDSA) | Mejor para proyectos nuevos — firmas pequeñas, rápido |
| RS256 | Asimétrico (RSA) | Ampliamente soportado, bueno para interoperabilidad |
| HS256 | Simétrico (HMAC) | Solo cuando el secreto es verdaderamente compartido y nunca expuesto |
| Ninguno | Nunca usar |
Para APIs públicas donde múltiples servicios verifican tokens, usa algoritmos asimétricos (ES256/RS256). La clave privada permanece en el servidor de autenticación; todos los demás servicios solo tienen la clave pública.
Expiración de tokens y estrategia de actualización
Los tokens de acceso de corta duración limitan el daño del robo de tokens. Usa un patrón de dos tokens:
- Token de acceso: expira en 5–15 minutos, enviado con cada solicitud API
- Token de actualización: expira en 7–30 días, almacenado de forma segura, usado solo para obtener nuevos tokens de acceso
// Emitir tokens
const tokenAcceso = jwt.sign(
{ sub: usuario.id, role: usuario.rol },
CLAVE_PRIVADA,
{ algorithm: "ES256", expiresIn: "15m" }
);
const tokenActualizacion = jwt.sign(
{ sub: usuario.id, jti: crypto.randomUUID() },
SECRETO_ACTUALIZACION,
{ expiresIn: "7d" }
);
Rotación de tokens de actualización
Cada vez que se usa un token de actualización, invalídalo y emite uno nuevo. Si se detecta que un token de actualización robado se usa dos veces, invalida toda la sesión:
async function actualizarTokens(tokenActualizacionAntiguo) {
const payload = jwt.verify(tokenActualizacionAntiguo, SECRETO_ACTUALIZACION);
// Verificar que el token no se haya usado antes (detección de reutilización)
const registroToken = await db.tokensActualizacion.findOne({ jti: payload.jti });
if (!registroToken || registroToken.usado) {
// Reutilización de token detectada — revocar toda la familia
await db.tokensActualizacion.revocarFamilia(payload.sub);
throw new Error("Reutilización de token detectada");
}
// Marcar como usado
await db.tokensActualizacion.marcarUsado(payload.jti);
// Emitir nuevo par
return emitirParDeTokens(payload.sub);
}
Almacenamiento seguro de JWTs
| Almacenamiento | Riesgo XSS | Riesgo CSRF | Recomendación |
|---|---|---|---|
localStorage | Alto | Ninguno | Nunca para tokens de autenticación |
sessionStorage | Alto | Ninguno | Nunca para tokens de autenticación |
| Memoria (var JS) | Bajo | Ninguno | Bueno para tokens de acceso |
Cookie HttpOnly | Ninguno | Medio | Mejor para tokens de actualización + tokens CSRF |
Almacena los tokens de acceso en la memoria de JavaScript. Almacena los tokens de actualización en cookies HttpOnly, Secure, SameSite=Strict.
// Establecer token de actualización como cookie HttpOnly
res.cookie("refresh_token", tokenActualizacion, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 días
path: "/auth/refresh", // solo enviado al endpoint de actualización
});
Claims que siempre debes validar
Además de verificar la firma, valida estos claims estándar:
jwt.verify(token, CLAVE_PUBLICA, {
algorithms: ["ES256"],
issuer: "https://auth.tudominio.com", // claim iss
audience: "https://api.tudominio.com", // claim aud
// exp se verifica automáticamente
// nbf se verifica automáticamente
});
| Claim | Significado | Validar siempre |
|---|---|---|
exp | Tiempo de expiración | Sí |
nbf | No antes de | Sí |
iss | Emisor | Sí |
aud | Audiencia | Sí |
sub | Sujeto (ID de usuario) | Sí, verificar en sesión |
jti | ID de JWT | Sí, para revocación |
Revocación de JWT
Los JWTs son sin estado — un token válido permanece válido hasta su expiración. Para revocación inmediata (cierre de sesión, cambio de contraseña, suspensión de cuenta), mantén una lista de bloqueo:
// Al cerrar sesión
await redis.setex(`revocado:${payload.jti}`, segundosTtlToken, "1");
// En cada solicitud
async function verificarToken(token) {
const payload = jwt.verify(token, CLAVE_PUBLICA, { algorithms: ["ES256"] });
const estaRevocado = await redis.exists(`revocado:${payload.jti}`);
if (estaRevocado) throw new Error("Token revocado");
return payload;
}
La corta expiración del token de acceso reduce cuánto tiempo necesitas mantener la lista de bloqueo.
Depuración de JWTs
Usa la Herramienta Decodificador JWT para inspeccionar cabeceras y payloads sin enviar tokens a servicios externos. Todo el descodificado ocurre en tu navegador.
Lista de verificación
- Usar ES256 o RS256 — nunca HS256 para APIs públicas
- Rechazar explícitamente
alg: "none"en la configuración de la biblioteca - Fijar el algoritmo en el servidor — nunca leerlo de la cabecera del token
- Establecer la expiración del token de acceso en 5–15 minutos
- Implementar rotación de tokens de actualización con detección de reutilización
- Almacenar tokens de actualización en cookies HttpOnly, tokens de acceso en memoria
- Validar
iss,aud,exp,nbfen cada solicitud - Implementar revocación basada en JTI para cierre de sesión/cambio de contraseña
- Nunca registrar JWTs completos — son credenciales de portador
Más herramientas de Cosyslabs
- Rough Estimator — Estima el esfuerzo de desarrollo e implementar autenticación JWT, rotación de tokens de actualización y capas de API seguras en tu proyecto.
- Routine Toolkit — Utilidades del día a día incluyendo calculadora de préstamos, calculadora de fechas y contador de palabras.
- Cosyslabs — El estudio detrás de Dev Tools !, PDF Convert All, Unit Convert All, y más.