Autenticación segura con JWT
Implementación empresarial de autenticación con JSON Web Tokens, incluyendo refresh tokens, revocación y mejores prácticas de seguridad.
¿Qué es JWT?
JSON Web Token (JWT) es un estándar abierto (RFC 7519) que define una forma compacta y autónoma de transmitir información de forma segura entre partes como un objeto JSON. Empresas como Auth0, Firebase, AWS Cognito y Okta utilizan JWT como base de sus sistemas de autenticación.
Estructura de un JWT
Un JWT consta de tres partes separadas por puntos:
header.payload.signature
1. Header
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "admin"
}
3. Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Implementación en Node.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
// Generar access token (corta duración)
function generateAccessToken(user) {
const payload = {
id: user.id,
email: user.email,
role: user.role
};
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '15m',
issuer: 'mi-app',
audience: 'mi-app-users'
});
}
// Generar refresh token (larga duración)
function generateRefreshToken(user) {
const payload = {
id: user.id,
tokenVersion: user.tokenVersion // Para revocación
};
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
expiresIn: '7d',
issuer: 'mi-app'
});
}
// Verificar token
function verifyToken(token, secret) {
try {
return jwt.verify(token, secret, {
issuer: 'mi-app',
audience: 'mi-app-users'
});
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expirado');
}
throw new Error('Token inválido');
}
}
// Login endpoint
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Buscar usuario
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Credenciales inválidas' });
}
// Verificar contraseña
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Credenciales inválidas' });
}
// Generar tokens
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// Guardar refresh token en base de datos
await RefreshToken.create({
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
// Enviar refresh token como httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 días
});
res.json({ accessToken });
});
Middleware de autenticación
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'Token no proporcionado' });
}
const token = authHeader.split(' ')[1]; // Bearer TOKEN
try {
const decoded = verifyToken(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: error.message });
}
}
// Middleware de autorización por rol
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'No autenticado' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'No autorizado' });
}
next();
};
}
// Uso
app.get('/api/profile', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
app.delete('/api/users/:id', authMiddleware, authorize('admin'), (req, res) => {
// Solo admins pueden eliminar usuarios
});
Refresh Token Rotation (Patrón empresarial)
Este patrón es usado por Auth0 y OAuth 2.0 para máxima seguridad.
app.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.cookies;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token no proporcionado' });
}
try {
// Verificar refresh token
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// Buscar token en base de datos
const storedToken = await RefreshToken.findOne({
userId: decoded.id,
token: refreshToken
});
if (!storedToken) {
// Posible ataque de reutilización de token
await RefreshToken.deleteMany({ userId: decoded.id });
return res.status(401).json({ error: 'Token inválido' });
}
// Buscar usuario
const user = await User.findById(decoded.id);
// Verificar versión del token (para revocación)
if (user.tokenVersion !== decoded.tokenVersion) {
return res.status(401).json({ error: 'Token revocado' });
}
// Eliminar refresh token antiguo
await RefreshToken.deleteOne({ _id: storedToken._id });
// Generar nuevos tokens (rotation)
const newAccessToken = generateAccessToken(user);
const newRefreshToken = generateRefreshToken(user);
// Guardar nuevo refresh token
await RefreshToken.create({
userId: user.id,
token: newRefreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
// Actualizar cookie
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken: newAccessToken });
} catch (error) {
return res.status(401).json({ error: 'Token inválido' });
}
});
Revocación de tokens
// Logout: revocar refresh token
app.post('/api/logout', authMiddleware, async (req, res) => {
const { refreshToken } = req.cookies;
// Eliminar refresh token de la base de datos
await RefreshToken.deleteOne({ token: refreshToken });
// Limpiar cookie
res.clearCookie('refreshToken');
res.json({ message: 'Logout exitoso' });
});
// Logout de todos los dispositivos
app.post('/api/logout-all', authMiddleware, async (req, res) => {
// Incrementar versión del token para invalidar todos los tokens existentes
await User.findByIdAndUpdate(req.user.id, {
$inc: { tokenVersion: 1 }
});
// Eliminar todos los refresh tokens del usuario
await RefreshToken.deleteMany({ userId: req.user.id });
res.json({ message: 'Sesiones cerradas en todos los dispositivos' });
});
Vulnerabilidades de seguridad y prevención
1. Algoritmo “none” (CVE-2015-9235)
// ❌ VULNERABLE: Acepta cualquier algoritmo
jwt.verify(token, secret);
// ✅ SEGURO: Especifica algoritmo permitido
jwt.verify(token, secret, { algorithms: ['HS256'] });
2. Secret débil
// ❌ MALO: Secret predecible
const secret = 'mi-secreto-123';
// ✅ BUENO: Secret fuerte y aleatorio
const secret = crypto.randomBytes(64).toString('hex');
// Almacenar en variable de entorno
3. Información sensible en payload
// ❌ NUNCA incluyas información sensible
const payload = {
id: user.id,
password: user.password, // ¡NO!
ssn: user.ssn // ¡NO!
};
// ✅ Solo información no sensible
const payload = {
id: user.id,
email: user.email,
role: user.role
};
Mejores prácticas empresariales
1. Usar HTTPS siempre
Siempre transmite tokens sobre HTTPS para prevenir interceptación (man-in-the-middle attacks).
2. Tiempo de expiración corto para access tokens
// Access token: 15 minutos
jwt.sign(payload, secret, { expiresIn: '15m' });
// Refresh token: 7 días
jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
3. Almacenamiento seguro
Backend:
- Variables de entorno para secrets
- Nunca en código fuente
- Usar servicios como AWS Secrets Manager, HashiCorp Vault
Frontend:
- Access token: memoria (variable JavaScript)
- Refresh token: httpOnly cookie (no accesible desde JavaScript)
- Nunca en localStorage (vulnerable a XSS)
4. Rate limiting
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 5, // 5 intentos
message: 'Demasiados intentos de login, intenta más tarde'
});
app.post('/api/login', loginLimiter, async (req, res) => {
// ...
});
5. Validación de claims
function verifyToken(token) {
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'mi-app',
audience: 'mi-app-users'
});
// Validar claims adicionales
if (!decoded.exp || decoded.exp < Date.now() / 1000) {
throw new Error('Token expirado');
}
if (!decoded.role || !['user', 'admin'].includes(decoded.role)) {
throw new Error('Rol inválido');
}
return decoded;
}
Integración con OAuth 2.0
// Ejemplo con Google OAuth
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
app.post('/api/auth/google', async (req, res) => {
const { token } = req.body;
try {
// Verificar token de Google
const ticket = await client.verifyIdToken({
idToken: token,
audience: process.env.GOOGLE_CLIENT_ID
});
const payload = ticket.getPayload();
// Buscar o crear usuario
let user = await User.findOne({ email: payload.email });
if (!user) {
user = await User.create({
email: payload.email,
name: payload.name,
googleId: payload.sub,
emailVerified: payload.email_verified
});
}
// Generar nuestros propios tokens JWT
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.json({ accessToken, refreshToken });
} catch (error) {
res.status(401).json({ error: 'Token de Google inválido' });
}
});
Conclusión
JWT es la solución estándar para autenticación en aplicaciones modernas, utilizada por empresas como Auth0, Firebase, AWS Cognito y Okta. Siguiendo estas mejores prácticas empresariales, puedes implementar un sistema de autenticación:
- Seguro: Con refresh token rotation y revocación
- Escalable: Stateless, ideal para microservicios
- Flexible: Compatible con OAuth 2.0 y SSO
- Robusto: Protegido contra vulnerabilidades comunes
La clave está en nunca comprometer la seguridad por conveniencia: usa HTTPS, secrets fuertes, tokens de corta duración y almacenamiento seguro.
Gustavo Leyva
Desarrollador de Software