Autenticación multifactor (MFA/2FA)
La autenticación multifactor (MFA, multi-factor authentication) añade una segunda (o tercera) capa de verificación después de la contraseña. Si la contraseña de un usuario se filtra o es adivinada, el atacante sigue sin poder acceder sin el segundo factor.

Factores de autenticación
Los factores se clasifican en tres categorías:
| Factor | Tipo | Ejemplos |
|---|---|---|
| Contraseña, PIN, pregunta secreta | Algo que sabes | Contraseña, frase de paso |
| TOTP, SMS, clave física | Algo que tienes | Google Authenticator, YubiKey, código SMS |
| Biometría | Algo que eres | Huella dactilar, reconocimiento facial |
Para que sea verdaderamente MFA, los factores deben ser de categorías distintas. Contraseña + PIN son dos factores de la misma categoría (ambos "algo que sabes") y no se consideran MFA real.
TOTP
El TOTP (Time-based One-Time Password) es el estándar más común para 2FA (two-factor authentication) sin SMS. Genera códigos de 6 dígitos que cambian cada 30 segundos, basándose en la hora actual y un secreto compartido. Funciona sin conexión a internet.
Estándar: RFC 6238. Implementado por Google Authenticator, Authy, 1Password, Bitwarden, etc.
Flujo de activación y uso
Activación:
- Usuario solicita activar 2FA.
- Servidor genera un secreto TOTP único para el usuario.
- Servidor devuelve el secreto como QR (otpauth://) y clave manual.
- Usuario escanea el QR con su app de autenticación.
- Usuario introduce el primer código para confirmar que la app funciona.
- Servidor activa 2FA para ese usuario y genera códigos de recuperación.
Login con 2FA activo:
- Usuario introduce usuario + contraseña → OK.
- Servidor devuelve un estado intermedio (no emite token aún).
- Usuario introduce el código de 6 dígitos de su app.
- Servidor verifica el código → emite token de sesión.
Implementación
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
import crypto from 'node:crypto';
// Paso 1: Generar el secreto y el QR para el usuario
export const initiate2FA = async (req, res) => {
const user = await findUserById(req.user.sub);
const secret = speakeasy.generateSecret({
name: `MiApp (${user.email})`, // Aparece en la app de autenticación
length: 32,
});
// Guardar el secreto temporalmente (aún no activado)
await db.savePendingTotpSecret(user.id, secret.base32);
const qrCodeDataUrl = await QRCode.toDataURL(secret.otpauth_url);
res.json({
qrCode: qrCodeDataUrl,
manualKey: secret.base32, // Para usuarios que no pueden escanear el QR
});
};
// Paso 2: Verificar el primer código e activar 2FA
export const confirm2FA = async (req, res) => {
const { token } = req.body; // Código de 6 dígitos introducido por el usuario
const pendingSecret = await db.getPendingTotpSecret(req.user.sub);
const isValid = speakeasy.totp.verify({
secret: pendingSecret,
encoding: 'base32',
token,
window: 1, // Acepta el código actual ± 30 segundos (margen de reloj)
});
if (!isValid) return res.status(400).json({ error: 'Código incorrecto. Inténtalo de nuevo.' });
// Activar 2FA y generar códigos de recuperación
const backupCodes = generateBackupCodes();
const hashedCodes = backupCodes.map(code =>
crypto.createHash('sha256').update(code).digest('hex')
);
await db.activateTotp(req.user.sub, pendingSecret, hashedCodes);
// Mostrar los códigos de recuperación una sola vez
res.json({
message: '2FA activado correctamente.',
backupCodes, // Mostrar una sola vez. El usuario debe guardarlos.
});
};
// Paso 3: Verificar el código TOTP durante el login
export const verifyTotpOnLogin = async (req, res) => {
const { pendingUserId, token } = req.body;
// pendingUserId viene de un token temporal emitido tras validar la contraseña
const user = await findUserById(pendingUserId);
if (!user?.totpEnabled) return res.status(400).json({ error: 'Usuario sin 2FA' });
const isValid = speakeasy.totp.verify({
secret: user.totpSecret,
encoding: 'base32',
token,
window: 1,
});
if (!isValid) return res.status(401).json({ error: 'Código 2FA incorrecto' });
// Emitir token de sesión completo
const accessToken = generateAccessToken(user);
res.json({ accessToken });
};
Códigos de recuperación
Siempre ofrece códigos de un solo uso (backup codes) al activar 2FA. Son la salvaguarda si el usuario pierde acceso a su app de autenticación (robo, pérdida, cambio de móvil).
export const generateBackupCodes = (count = 8) => {
// Formato legible: XXXXX-XXXXX
return Array.from({ length: count }, () => {
const part1 = crypto.randomBytes(3).toString('hex').toUpperCase();
const part2 = crypto.randomBytes(3).toString('hex').toUpperCase();
return `${part1}-${part2}`;
});
// Ejemplo: ['A3F2B1-C9D4E5', 'F6A7B8-C9D0E1', ...]
};
// Usar un código de recuperación
export const useBackupCode = async (userId, inputCode) => {
const user = await findUserById(userId);
const inputHash = crypto.createHash('sha256').update(inputCode).digest('hex');
const codeIndex = user.backupCodeHashes.findIndex(h => h === inputHash);
if (codeIndex === -1) return false;
// Eliminar el código usado (son de un solo uso)
user.backupCodeHashes.splice(codeIndex, 1);
await db.updateBackupCodes(userId, user.backupCodeHashes);
return true;
};
SMS OTP
El usuario recibe un código de 6 dígitos por SMS al intentar hacer login.
La ventaja es que es más familiar para usuarios no técnicos, no requiere instalar ninguna app.
Desventajas:
- Menos seguro que TOTP. Vulnerable a ataques de SIM swapping (el atacante convence a la operadora para transferir el número a otra SIM) e interceptación de SS7 (protocolo de la red telefónica con vulnerabilidades conocidas).
- Requiere un servicio externo de pago como, por ejemplo, Twilio.
- No funciona sin cobertura/roaming.
import twilio from 'twilio';
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
export const sendSmsOtp = async (phoneNumber) => {
const otp = Math.floor(100000 + Math.random() * 900000).toString(); // 6 dígitos
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutos
// Guardar OTP hasheado en BD con TTL
await db.saveSmsOtp(phoneNumber, hashOtp(otp), expiresAt);
await client.messages.create({
body: `Tu código de verificación es: ${otp}. Expira en 10 minutos.`,
from: process.env.TWILIO_PHONE_NUMBER,
to: phoneNumber,
});
};
Usa TOTP siempre que puedas. Utiliza los SMS para casos donde sea imprescindible por experiencia de usuario (UX) o porque no haya otra alternativa.
WebAuthn / FIDO2 (mención)
Web Authentication (WebAuthn) es el estándar más moderno y seguro. Usa criptografía de clave pública: el dispositivo del usuario (smartphone, YubiKey, TouchID, Windows Hello) genera un par de claves. La clave privada nunca sale del dispositivo. Elimina completamente el phishing.
Su implementación es más compleja; librerías como @simplewebauthn/server abstraen gran parte del proceso.
Librerías relevantes
| Librería | Descripción |
|---|---|
speakeasy | Generación y verificación de TOTP/HOTP. Madura y ampliamente usada. |
otplib | Alternativa moderna a speakeasy con mejor soporte de TypeScript. |
qrcode | Genera QR codes (necesario para el flujo de activación de TOTP). |
twilio | SDK oficial de Twilio para envío de SMS OTP. |
@simplewebauthn/server | Implementación de WebAuthn/FIDO2 en Node.js. |