Двухфакторная аутентификация (2FA)
2FA (Two-Factor Authentication) — способ аутентификации, требующий подтверждения личности двумя независимыми факторами: чем-то, что пользователь знает (пароль) и чем-то, что пользователь имеет (OTP, FIDO2-ключ) или является (биометрия).
Зачем нужно
Утечка базы паролей — не катастрофа, если включён 2FA. По статистике Google, 2FA блокирует 99% автоматизированных атак (credential stuffing, brute force) и 76% целевых фишинговых атак. Для приложений с ценными данными 2FA — обязательная мера.
Где используется
- Административные панели и SaaS с критическими данными
- Финансовые операции (дополнительное подтверждение перевода)
- OAuth-провайдеры (Google, GitHub) — 2FA для защиты downstream сервисов
- Zero Trust среды — подтверждение каждого нового устройства
Основной контент
Типы второго фактора
TOTP (HOTP) → Google Authenticator, Authy — одноразовые коды (RFC 6238)
SMS OTP → Уязвимо к SIM-swap атакам, но лучше, чем ничего
Email OTP → Удобно для пользователей, менее безопасно, чем TOTP
FIDO2/WebAuthn → Аппаратные ключи (YubiKey), биометрия — наиболее надёжно
Push Notification → Duo Security, Okta Verify
Реализация TOTP (Node.js + speakeasy)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// 1. Генерация секрета при включении 2FA
app.post('/api/2fa/setup', authenticate, async (req, res) => {
const secret = speakeasy.generateSecret({
name: `MyApp (${req.user.email})`,
length: 32,
});
// Сохранить secret.base32 в БД (пока не подтверждён)
await db.query(
'UPDATE users SET totp_secret_pending = $1 WHERE id = $2',
[secret.base32, req.user.id]
);
const qrUrl = await QRCode.toDataURL(secret.otpauth_url);
res.json({ qrCode: qrUrl, secret: secret.base32 });
});
// 2. Верификация кода при активации
app.post('/api/2fa/verify', authenticate, async (req, res) => {
const { token } = req.body;
const { totp_secret_pending } = await getUserById(req.user.id);
const verified = speakeasy.totp.verify({
secret: totp_secret_pending,
encoding: 'base32',
token,
window: 1, // Допускать ±30 секунд
});
if (!verified) return res.status(400).json({ error: 'Invalid code' });
await db.query(
'UPDATE users SET totp_secret = totp_secret_pending, totp_enabled = true WHERE id = $1',
[req.user.id]
);
res.json({ success: true });
});
// 3. Проверка при логине
app.post('/api/auth/login', async (req, res) => {
const user = await verifyCredentials(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
if (user.totp_enabled) {
// Вернуть промежуточный токен, запросить OTP
const tempToken = signTempToken(user.id);
return res.json({ requiresMfa: true, tempToken });
}
const sessionToken = signSessionToken(user.id);
res.json({ token: sessionToken });
});
Backup коды (recovery codes)
const crypto = require('crypto');
function generateBackupCodes(count = 8) {
return Array.from({ length: count }, () => crypto.randomBytes(4).toString('hex').toUpperCase()
); // Формат: "A1B2C3D4"
}
// Хранить hashed (bcrypt) в БД
// Показать пользователю один раз при настройке
Частые ошибки
- SMS-only 2FA без альтернатив — уязвимо к SIM-swap атакам
- Отсутствие rate limiting на проверку OTP-кода — 6-значный код можно перебрать
- Хранение TOTP-секрета в plaintext — шифровать как и пароли
- Отсутствие backup-кодов — пользователь теряет телефон и доступ к аккаунту навсегда