Saltar al contenido principal

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) o scrypt. 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, secure y SameSite.
  • 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 audit sin vulnerabilidades críticas).

Librerías relevantes

LibreríaDescripción
helmetConfigura cabeceras HTTP de seguridad (CSP, HSTS, X-Frame-Options, etc.).
express-rate-limitRate limiting por IP para Express. Sin dependencias externas.
rate-limiter-flexibleRate limiting avanzado con Redis, por IP y por usuario.
express-validatorValidació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