Двухфакторная аутентификация (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-кодов — пользователь теряет телефон и доступ к аккаунту навсегда

Связанные темы

Ресурсы