¿Qué es la autenticación JWT?
JSON Web Token (JWT, pronunciado "yot") es un estándar compacto y autocontenido para transmitir información de autenticación y autorización entre partes como un token firmado digitalmente. Una vez que un usuario inicia sesión, el servidor emite un JWT que contiene claims sobre ese usuario. El cliente almacena el token y lo envía con cada solicitud subsiguiente. El servidor verifica la firma del token para confirmar la autenticidad — sin necesidad de consultar la base de datos.
Los JWTs están definidos por RFC 7519 y son el mecanismo de autenticación dominante para APIs REST, aplicaciones de página única y arquitecturas de microservicios. Son stateless por diseño: el servidor no necesita almacenar datos de sesión, lo que hace que el escalado horizontal sea sencillo.
Cómo funciona JWT
El flujo completo de autenticación JWT funciona así:
- Login: El usuario envía sus credenciales (usuario + contraseña) al endpoint de login del servidor.
- Emisión del token: El servidor valida las credenciales contra la base de datos. Si son correctas, firma un JWT que contiene el ID del usuario, roles y una marca de tiempo de expiración, y luego devuelve el token al cliente.
- Almacenamiento del token: El cliente almacena el token (en memoria, localStorage o una cookie httpOnly — más sobre esto después).
- Solicitudes autenticadas: Para cada solicitud API subsiguiente, el cliente incluye el JWT en el encabezado
Authorization:Authorization: Bearer <token>. - Verificación: El servidor extrae el token del encabezado, verifica la firma usando su clave secreta, comprueba la expiración y lee los claims — todo sin una consulta a la base de datos.
- Respuesta: Si el token es válido, el servidor procesa la solicitud. Si no (expirado, manipulado, ausente), devuelve un
401 Unauthorized.
Estructura de un JWT explicada
Un JWT consiste en tres partes codificadas en Base64URL separadas por puntos:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDkzMDAwMDAsImV4cCI6MTcwOTMwMzYwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Las tres partes son:
1. Header
{
"alg": "HS256", // Algoritmo de firma (HS256, RS256, ES256)
"typ": "JWT" // Tipo de token
}
2. Payload (Claims)
{
"userId": "123456",
"email": "user@example.com",
"role": "admin",
"iat": 1709300000, // Emitido en (timestamp Unix)
"exp": 1709303600 // Expiración (1 hora desde la emisión)
}
Nombres de claims estándar: iss (emisor), sub (sujeto), aud (audiencia), exp (expiración), iat (emitido en), jti (ID de JWT para revocación). El payload está codificado en Base64URL, no cifrado — cualquiera puede decodificarlo. Nunca almacenes contraseñas, números de tarjetas de crédito u otros datos sensibles en el payload.
3. Firma
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
La firma prueba que el token fue emitido por tu servidor y no ha sido manipulado. Si cualquier carácter en el header o payload cambia, la verificación de la firma falla.
Implementando JWT en Node.js
Instala el paquete jsonwebtoken:
npm install jsonwebtoken bcryptjs express
Crea un ejemplo completo de login y ruta protegida:
// auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const express = require('express');
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET; // Debe ser una cadena larga y aleatoria
const JWT_EXPIRES_IN = '1h'; // Mantener los access tokens de corta duración
// Ruta de login — emitir un JWT en autenticación exitosa
router.post('/login', async (req, res) => {
const { email, password } = req.body;
// 1. Buscar el usuario en tu base de datos
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 2. Comparar contraseña con el hash almacenado
const validPassword = await bcrypt.compare(password, user.passwordHash);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 3. Firmar el JWT — incluir solo claims no sensibles
const token = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN, algorithm: 'HS256' }
);
res.json({ token, expiresIn: 3600 });
});
// Middleware para verificar JWT en rutas protegidas
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
// Especificar explícitamente el algoritmo para prevenir ataques de confusión de algoritmo
const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
// Ejemplo de ruta protegida
router.get('/profile', authenticateToken, (req, res) => {
res.json({ userId: req.user.userId, email: req.user.email });
});
module.exports = { router, authenticateToken };
Almacenar JWT en React
Dónde almacenes el JWT en el navegador afecta significativamente la postura de seguridad de tu aplicación. Las dos opciones principales son localStorage y cookies httpOnly:
| Propiedad | localStorage | Cookie httpOnly |
|---|---|---|
| Vulnerabilidad XSS | Alta — cualquier script puede leerlo | Baja — inaccesible para JavaScript |
| Vulnerabilidad CSRF | Ninguna | Sí — mitigar con SameSite + token CSRF |
| Se envía automáticamente | No — debe agregarse al encabezado manualmente | Sí — el navegador lo envía con cada solicitud |
| Accesible desde JS | Sí | No (flag httpOnly) |
| Recomendado para | Apps de bajo riesgo, datos públicos | Apps en producción con datos de usuario |
Para aplicaciones en producción, prefiere cookies httpOnly. Tu servidor configura la cookie en el login y el navegador la envía automáticamente, sin que JavaScript toque nunca el valor del token.
Si usas localStorage (por ejemplo, por simplicidad durante el desarrollo), almacena el token en memoria cuando sea posible e implementa una Content Security Policy robusta para mitigar el riesgo de XSS:
// React — enviar JWT desde localStorage
async function fetchProfile() {
const token = localStorage.getItem('authToken');
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401) {
// Token expirado — redirigir al login o refrescar
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return response.json();
}
Estrategia de Refresh Token
Los access tokens de corta duración (15 minutos a 1 hora) minimizan la ventana de robo de tokens, pero forzar a los usuarios a iniciar sesión cada hora es una experiencia terrible. La solución es un par de refresh tokens:
- Access token: De corta duración (15 min – 1 hora), enviado con cada solicitud API en el encabezado
Authorization. Almacenado en memoria o localStorage. - Refresh token: De larga duración (7 – 30 días), usado solo para obtener nuevos access tokens. Siempre almacenado en una cookie httpOnly, Secure, SameSite=Strict. Debe rotarse en cada uso.
// Servidor: emitir ambos tokens en el login
router.post('/login', async (req, res) => {
// ... validar credenciales ...
const accessToken = jwt.sign(
{ userId: user._id, role: user.role },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '30d' }
);
// Almacenar hash del refresh token en base de datos para soporte de revocación
await RefreshToken.create({ userId: user._id, token: hashToken(refreshToken) });
// Enviar refresh token como cookie httpOnly
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 días en ms
});
res.json({ accessToken });
});
// Servidor: endpoint de refresh
router.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Opcional: verificar que el hash del token existe en la BD (permite revocación)
const newAccessToken = jwt.sign(
{ userId: decoded.userId },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch {
res.status(403).json({ error: 'Invalid refresh token' });
}
});
Mejores prácticas de seguridad JWT
- Mantén los access tokens de corta duración: 15 minutos a 1 hora. Los tokens robados se vuelven inútiles después de expirar.
- Usa secretos fuertes: Tu
JWT_SECRETdebe tener al menos 256 bits de datos aleatorios. Genéralo conopenssl rand -hex 32. Nunca lo hardcodees. - Siempre especifica el algoritmo: Pasa
{ algorithms: ['HS256'] }ajwt.verify(). Esto previene el ataque de confusión de algoritmo (none) donde un atacante establece"alg": "none"en el header. - Solo HTTPS en producción: Los JWTs en tránsito son tan seguros como tu capa de transporte. Siempre usa TLS en producción.
- Almacena refresh tokens en cookies httpOnly: No en localStorage ni sessionStorage.
- Implementa revocación de tokens: Para logout y eventos de seguridad, mantén una lista de bloqueo de corta duración (Redis funciona bien) o usa refresh tokens rotativos con seguimiento en base de datos.
- Valida todos los claims: Verifica
exp(expiración),iss(emisor) yaud(audiencia) en cada solicitud. La libreríajsonwebtokenlo hace automáticamente cuando pasas las opciones correctas.
Errores comunes con JWT
Almacenar JWTs en localStorage en apps de alto riesgo
Cualquier vulnerabilidad XSS — incluso un script de terceros que hayas cargado — puede leer localStorage y exfiltrar todos los tokens. Para aplicaciones que manejan pagos, datos personales o acciones de administrador, usa cookies httpOnly exclusivamente.
Sin expiración en los tokens
Los tokens sin un claim exp son válidos para siempre. Un token filtrado se convierte en una puerta trasera permanente. Siempre configura expiresIn al firmar.
El ataque de algoritmo none
Las primeras librerías JWT aceptaban "alg": "none" en el header, efectivamente aceptando tokens sin firmar como válidos. Siempre pasa la opción algorithms a jwt.verify() para restringir qué algoritmos son aceptables:
// Siempre haz esto — nunca permitas el algoritmo "none"
jwt.verify(token, secret, { algorithms: ['HS256'] });
Datos sensibles en el payload
El payload del JWT está codificado en Base64URL, no cifrado. Cualquier parte con el token puede decodificar y leer el payload sin conocer el secreto. Nunca incluyas contraseñas, números de seguro social, números de tarjetas de crédito o detalles internos del sistema en los claims JWT.
Usar el mismo secreto para access y refresh tokens
Si usas el mismo secreto para ambos tipos de token, un access token comprometido podría potencialmente usarse como refresh token. Usa secretos separados para cada tipo de token almacenados en variables de entorno separadas.
Decodifica tus tokens JWT
¿Necesitas inspeccionar un JWT para verificar sus claims, expiración o estructura? Pégalo en el Decodificador JWT de devbit.dev para decodificar instantáneamente el header y payload, verificar el tiempo de expiración y validar el formato del token — todo en tu navegador sin enviar datos a ningún servidor.
Decodifica e inspecciona cualquier JWT al instante
Pega un token JWT para decodificar su header, payload y claims. Verifica expiración, algoritmo y estructura. 100% del lado del cliente — tu token nunca sale de tu navegador.
Abrir decodificador JWT →Herramientas de desarrollo relacionadas
- Decodificador e inspector JWT — decodifica cualquier JSON Web Token al instante, verifica expiración y claims
- Depurador de errores CORS — soluciona errores CORS con el encabezado Authorization en Express, Nginx
- Generador de Hash — genera hashes SHA-256, MD5 y bcrypt para contraseñas y tokens
- Códigos de estado HTTP — entiende 401 Unauthorized vs 403 Forbidden y otros códigos de autenticación
- Inspector ENV — valida JWT_SECRET y otras variables de entorno en tu archivo .env
- Ver todas las herramientas gratuitas para desarrolladores