Buenas prácticas
Esta sección recoge principios transversales que aplican independientemente del mecanismo de autenticación elegido.
HTTPS es obligatorio
Ningún mecanismo de autenticación es seguro sobre HTTP. Contraseñas, tokens y cookies viajan en texto claro y pueden ser interceptados trivialmente con un ataque de man-in-the-middle.
- En producción: usa un certificado TLS. Let's Encrypt es gratuito y automatizable.
- Redirige siempre HTTP → HTTPS (código 301).
- Activa
Strict-Transport-Security(HSTS) para que los navegadores recuerden usar HTTPS:
// Con Helmet (recomendado)
import helmet from 'helmet';
app.use(helmet()); // Incluye HSTS, CSP, X-Frame-Options y más
// O manualmente
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
Rate limiting y protección contra fuerza bruta
Sin rate limiting, un atacante puede probar millones de contraseñas automáticamente. Aplica límites especialmente en endpoints de login, registro y recuperación de contraseña.
import rateLimit from 'express-rate-limit';
// Límite general para la API
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100,
standardHeaders: true,
legacyHeaders: false,
});
// Límite estricto para autenticación
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // Solo 10 intentos por IP cada 15 minutos
message: { error: 'Demasiados intentos. Espera 15 minutos.' },
skipSuccessfulRequests: true, // No contar los logins exitosos
});
app.use('/api/', apiLimiter);
app.use('/auth/login', authLimiter);
app.use('/auth/refresh', authLimiter);
Para rate limiting avanzado (por IP + usuario, con Redis para múltiples instancias):
import { RateLimiterRedis } from 'rate-limiter-flexible';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
const loginLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_fail',
points: 5, // 5 intentos fallidos
duration: 60 * 15, // en 15 minutos
blockDuration: 60 * 15, // bloquear 15 minutos
});
async function loginHandler(req, res) {
try {
await loginLimiter.consume(req.ip);
} catch {
return res.status(429).json({ error: 'Demasiados intentos fallidos. Espera 15 minutos.' });
}
// ... lógica de login
}
Gestión segura de secretos
# .env — NUNCA subir al repositorio
JWT_ACCESS_SECRET=cadena_aleatoria_minimo_32_chars_para_HS256
JWT_REFRESH_SECRET=otra_cadena_diferente_a_la_anterior
SESSION_SECRET=otra_cadena_diferente
GOOGLE_CLIENT_SECRET=obtenido_desde_google_cloud_console
// Validar que los secretos están configurados al arrancar la app
export const validateEnvSecrets = () => {
const required = ['JWT_ACCESS_SECRET', 'JWT_REFRESH_SECRET', 'SESSION_SECRET'];
for (const key of required) {
if (!process.env[key] || process.env[key].length < 32) {
throw new Error(`Variable de entorno insegura o ausente: ${key}`);
}
}
};
validateEnvSecrets(); // Llamar antes de iniciar el servidor
En producción, usa un secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) en lugar de archivos .env.
Validación y saneamiento de inputs
Nunca confíes en los datos que llegan del cliente. Valida el formato y tipo antes de procesarlos:
import { body, validationResult } from 'express-validator';
app.post('/auth/login', [
body('username').isEmail().normalizeEmail(),
body('password').isLength({ min: 8, max: 128 }).trim(),
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// ... continuar con el login
});
Mensajes de error que no filtren información
Los mensajes de error en autenticación no deben revelar qué parte de las credenciales es incorrecta:
// ❌ Se le dice al atacante que el usuario existe (puede filtrar una lista de usuarios existentes).
if (!user) return res.status(401).json({ error: 'Usuario no encontrado' });
if (!valid) return res.status(401).json({ error: 'Contraseña incorrecta' });
// ✅ Mensaje genérico sin información para el atacante.
if (!user || !valid) return res.status(401).json({ error: 'Credenciales incorrectas' });
Logging de eventos de seguridad
Registra los eventos de autenticación relevantes para detectar ataques y facilitar la auditoría:
// Eventos a registrar:
// - Login exitoso (userId, IP, userAgent, timestamp)
// - Login fallido (username intentado, IP, timestamp)
// - Logout
// - Cambio de contraseña
// - Activación/desactivación de 2FA
// - Generación/revocación de API Keys
// - Detección de reuso de refresh token
logger.info('auth.login.success', { userId: user.id, ip: req.ip, ua: req.headers['user-agent'] });
logger.warn('auth.login.failure', { username: req.body.username, ip: req.ip });
Checklist para la implementación de autenticación
- Contraseñas hasheadas con
argon2id,bcrypt(≥12 rounds) oscrypt. Nunca en texto plano. - HTTPS en producción con HSTS activado.
- Secretos en variables de entorno o secrets manager. Nunca en el código fuente.
- Secretos ausentes o débiles provocan error al arrancar el servidor.
- Rate limiting en todos los endpoints de autenticación.
- Cabeceras de seguridad configuradas (Helmet).
- Cookies con
httpOnly,secureySameSite. - JWT con tiempo de expiración corto (≤15 min) + refresh tokens rotativos.
- Logout invalida el refresh token en la BD.
- Mensajes de error genéricos que no filtren información.
- Inputs validados y saneados en servidor.
- Logs de intentos de autenticación fallidos y eventos de seguridad.
- 2FA disponible (o requerido) para cuentas sensibles.
- Dependencias actualizadas (
npm auditsin vulnerabilidades críticas).
Librerías relevantes
| Librería | Descripción |
|---|---|
helmet | Configura cabeceras HTTP de seguridad (CSP, HSTS, X-Frame-Options, etc.). |
express-rate-limit | Rate limiting por IP para Express. Sin dependencias externas. |
rate-limiter-flexible | Rate limiting avanzado con Redis, por IP y por usuario. |
express-validator | Validación y saneamiento de inputs en Express. |
npm install helmet express-rate-limit express-validator
# Para rate limiting con Redis:
npm install rate-limiter-flexible redis