← Назад к вопросам

Как работает мультифакторная аутентификация?

2.0 Middle🔥 171 комментариев
#Безопасность и аутентификация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Мультифакторная аутентификация (MFA): механизм и реализация

Мультифакторная аутентификация — это критическое требование безопасности современных систем. Рассмотрю все аспекты реализации и best practices.

Основной принцип MFA

Определение: Нечто, что ты знаешь + Нечто, что ты имеешь + Нечто, что ты являешься

Традиционная аутентификация:
[Пароль] → вход
    ↓
    Проблема: если пароль скомпрометирован → полный доступ

Мультифакторная аутентификация:
[Пароль] + [OTP/Биометрия] → вход
    ↓
    Даже если пароль украден → без второго фактора доступа нет

Факторы аутентификации

Фактор 1: Знание (Something You Know)

✅ Пароль
✅ PIN
✅ Ответ на секретный вопрос
✅ Пассфраза

Особенности:
- Самый уязвимый фактор
- Может быть украден, подслушан
- Легко забыть

Фактор 2: Владение (Something You Have)

✅ Мобильный телефон (SMS, Push notifications)
✅ Аппаратный токен (U2F ключ, YubiKey)
✅ Мобильное приложение (Authenticator App)
✅ Email
✅ Смарт-карта

Особенности:
- Более надежный чем пароль
- Невозможно украсть удаленно
- Может быть потеряно

Фактор 3: Биометрия (Something You Are)

✅ Отпечаток пальца
✅ Распознавание лица
✅ Радужная оболочка глаза
✅ Голос

Особенности:
- Практически невозможно подделать
- Не требует запоминания
- Требует специального оборудования

Метод 1: OTP (One-Time Password)

Time-based OTP (TOTP)

Схема работы:
1. При регистрации генерируется секретный ключ
2. Клиент (Google Authenticator, Authy) генерирует 6-значный код каждые 30 сек
3. При входе вводит текущий код
4. Сервер проверяет код

Формула: HMAC-SHA1(secret_key, time_window)

Пример реализации:

import pyotp
import qrcode
from datetime import datetime, timedelta

class MFAService:
    @staticmethod
    def generate_secret():
        """Генерируем секретный ключ"""
        return pyotp.random_base32()  # Base32 строка
    
    @staticmethod
    def get_totp_uri(email, secret, issuer="MyApp"):
        """Получаем provisioning URI для QR code"""
        totp = pyotp.TOTP(secret)
        return totp.provisioning_uri(
            name=email,
            issuer_name=issuer
        )
    
    @staticmethod
    def verify_totp(secret, token):
        """Проверяем TOTP токен"""
        totp = pyotp.TOTP(secret)
        return totp.verify(token, valid_window=1)

# Использование
class User(Base):
    __tablename__ = 'users'
    
    id = Column(UUID, primary_key=True)
    email = Column(String(255), unique=True)
    password_hash = Column(String(255))
    mfa_secret = Column(String(32), nullable=True)  # Хранить зашифрованным!
    mfa_enabled = Column(Boolean, default=False)
    backup_codes = Column(JSON, nullable=True)  # Коды для восстановления

# При регистрации MFA
def enable_mfa(user_id, db: Session):
    user = db.query(User).filter(User.id == user_id).first()
    secret = MFAService.generate_secret()
    
    # Генерируем QR code
    qr_uri = MFAService.get_totp_uri(user.email, secret)
    
    # Генерируем backup коды (10 одноразовых кодов)
    backup_codes = [secrets.token_hex(4) for _ in range(10)]
    
    # Временно сохраняем (требует подтверждения)
    user.mfa_secret_pending = encrypt(secret)
    user.backup_codes_pending = backup_codes
    db.commit()
    
    return {
        "qr_code": qr_uri,
        "secret": secret,  # На случай если QR не сканируется
        "backup_codes": backup_codes
    }

# При подтверждении MFA
def confirm_mfa(user_id, totp_token, db: Session):
    user = db.query(User).filter(User.id == user_id).first()
    secret = decrypt(user.mfa_secret_pending)
    
    # Проверяем TOTP
    if not MFAService.verify_totp(secret, totp_token):
        raise Exception("Invalid TOTP token")
    
    # Активируем MFA
    user.mfa_secret = user.mfa_secret_pending
    user.mfa_enabled = True
    user.backup_codes = user.backup_codes_pending
    user.mfa_secret_pending = None
    user.backup_codes_pending = None
    db.commit()

# При входе
def login_with_mfa(email, password, totp_token, db: Session):
    user = db.query(User).filter(User.email == email).first()
    
    # Шаг 1: Проверяем пароль
    if not verify_password(password, user.password_hash):
        raise Exception("Invalid password")
    
    # Шаг 2: Если MFA включена, проверяем TOTP
    if user.mfa_enabled:
        secret = decrypt(user.mfa_secret)
        if not MFAService.verify_totp(secret, totp_token):
            raise Exception("Invalid TOTP token")
    
    # Шаг 3: Выполняем вход
    return create_session(user)

Метод 2: SMS OTP

Проблемы:

- SMS может быть перехвачен (SIM hijacking)
- Не работает без сети
- Медленнее чем TOTP
- Зависит от оператора связи

✅ Лучше использовать TOTP + SMS как backup

Реализация:

from twilio.rest import Client

class SMSMFAService:
    def __init__(self, account_sid, auth_token):
        self.client = Client(account_sid, auth_token)
    
    def send_otp(self, phone_number):
        """Генерируем и отправляем OTP по SMS"""
        otp = random.randint(100000, 999999)
        
        # Сохраняем в кэш с TTL 10 минут
        redis.setex(
            f"mfa_sms:{phone_number}",
            600,  # TTL 10 минут
            otp
        )
        
        # Отправляем SMS
        self.client.messages.create(
            body=f"Your verification code: {otp}",
            from_="+1234567890",
            to=phone_number
        )
        
        return {"status": "sent"}
    
    def verify_otp(self, phone_number, otp):
        """Проверяем OTP"""
        stored_otp = redis.get(f"mfa_sms:{phone_number}")
        if not stored_otp or stored_otp.decode() != otp:
            return False
        
        # Удаляем использованный OTP
        redis.delete(f"mfa_sms:{phone_number}")
        return True

Метод 3: Push Notifications

Как работает:

1. Пользователь вводит email/пароль в приложение
2. Сервер отправляет push notification на телефон
3. Пользователь нажимает "Approve" в приложении
4. Приложение отправляет подтверждение серверу
5. Сервер проверяет и выполняет вход

Преимущества:
✅ Быстро и удобно
✅ Не нужно вводить коды
✅ Высокий уровень безопасности

Реализация (Firebase Cloud Messaging):

from firebase_admin import messaging

class PushMFAService:
    def send_mfa_challenge(self, user_id, user_device_token):
        """Отправляем push challenge"""
        # Генерируем challenge ID
        challenge_id = uuid.uuid4()
        
        # Сохраняем вызов в кэш (TTL 2 минуты)
        redis.setex(
            f"mfa_challenge:{challenge_id}",
            120,
            json.dumps({
                "user_id": str(user_id),
                "created_at": datetime.utcnow().isoformat(),
                "approved": False
            })
        )
        
        # Отправляем push notification
        message = messaging.Message(
            data={
                "challenge_id": str(challenge_id),
                "type": "mfa_approval",
                "message": "Approve login from new device?"
            },
            token=user_device_token
        )
        messaging.send(message)
        
        return {"challenge_id": str(challenge_id)}
    
    def check_approval(self, challenge_id):
        """Проверяем, одобрил ли пользователь"""
        data = redis.get(f"mfa_challenge:{challenge_id}")
        if not data:
            return {"status": "expired"}
        
        challenge = json.loads(data)
        if challenge["approved"]:
            redis.delete(f"mfa_challenge:{challenge_id}")
            return {"status": "approved", "user_id": challenge["user_id"]}
        
        return {"status": "pending"}

Метод 4: Hardware Tokens (U2F / WebAuthn)

Как работает:

1. Пользователь регистрирует физический ключ (YubiKey)
2. При входе нажимает кнопку на ключе
3. Ключ генерирует криптографическую подпись
4. Сервер проверяет подпись
5. Вход выполнен

Преимущества:
✅ Очень безопасно (криптография)
✅ Невозможно фишинг (ключ может проверить домен)
✅ Быстро

Реализация с WebAuthn:

from webauthn import (
    generate_registration_options,
    verify_registration_response,
    generate_authentication_options,
    verify_authentication_response,
    options_to_json
)
from webauthn.helpers.structs import (
    AuthenticatorSelectionCriteria,
    UserVerificationRequirement,
    AttestationConveyancePreference
)

class WebAuthnService:
    def register_start(self, user_id, user_email):
        """Начало регистрации WebAuthn"""
        options = generate_registration_options(
            rp_id="example.com",
            rp_name="My App",
            user_id=str(user_id),
            user_name=user_email,
            user_display_name=user_email,
            supported_algs=[-7, -257],  # ES256, RS256
            authenticator_selection=AuthenticatorSelectionCriteria(
                authenticator_attachment="cross-platform",  # Hardware token
                resident_key="discouraged",
                user_verification=UserVerificationRequirement.PREFERRED
            )
        )
        
        # Сохраняем challenge в кэш
        redis.setex(
            f"webauthn_challenge:{user_id}",
            600,  # 10 минут
            options.challenge
        )
        
        return options_to_json(options)
    
    def register_complete(self, user_id, credential):
        """Завершение регистрации WebAuthn"""
        # Получаем challenge
        challenge = redis.get(f"webauthn_challenge:{user_id}")
        
        # Проверяем регистрацию
        verified_registration = verify_registration_response(
            credential=credential,
            expected_challenge=challenge,
            expected_origin="https://example.com",
            expected_rp_id="example.com"
        )
        
        if not verified_registration.verified:
            raise Exception("Invalid registration")
        
        # Сохраняем credential
        db.add(WebAuthnCredential(
            user_id=user_id,
            credential_id=verified_registration.credential_id,
            public_key=verified_registration.credential_public_key
        ))
        db.commit()

Метод 5: Backup Codes

Для восстановления доступа:

class BackupCodesService:
    @staticmethod
    def generate_backup_codes(count=10):
        """Генерируем 10 одноразовых кодов"""
        codes = [secrets.token_hex(4) for _ in range(count)]  # 8 символов
        return codes
    
    @staticmethod
    def hash_code(code):
        """Хешируем код перед сохранением"""
        return hashlib.sha256(code.encode()).hexdigest()
    
    @staticmethod
    def verify_backup_code(user, code):
        """Проверяем и используем backup code"""
        code_hash = BackupCodesService.hash_code(code)
        
        for stored_hash in user.backup_codes:
            if stored_hash == code_hash:
                # Удаляем использованный код
                user.backup_codes.remove(stored_hash)
                db.commit()
                return True
        
        return False

# При входе
def login_with_backup_code(email, password, backup_code):
    user = db.query(User).filter(User.email == email).first()
    
    # Проверяем пароль
    if not verify_password(password, user.password_hash):
        raise Exception("Invalid password")
    
    # Если TOTP не доступна, но есть backup code
    if BackupCodesService.verify_backup_code(user, backup_code):
        return create_session(user)
    
    raise Exception("Invalid backup code")

Полный flow: регистрация и вход

Регистрация:

1. Email + Пароль → создаем пользователя
2. Вход в аккаунт
3. Переходим на страницу "Enable MFA"
4. Сканируем QR code в Google Authenticator
5. Вводим 6-значный код для подтверждения
6. Получаем 10 backup codes
7. MFA активирована ✅

Вход с MFA:

Шаг 1: Email & Пароль
   ↓ (проверяем в БД)
   
Шаг 2: Запрашиваем TOTP/Push
   ↓ (пользователь одобряет)
   
Шаг 3: Создаем сессию
   ↓
   Вход успешный ✅

Безопасность MFA

Угрозы:

1. SIM Hijacking (для SMS)
   ✅ Решение: использовать TOTP вместо SMS

2. Фишинг (пользователь вводит код на поддельном сайте)
   ✅ Решение: WebAuthn (проверяет домен)

3. Перехват OTP в пути
   ✅ Решение: HTTPS, OTP с TTL 30 сек

4. Потеря доступа (потеря телефона)
   ✅ Решение: Backup codes

Best Practices

1. Используйте комбинацию факторов

✅ TOTP + Email backup
✅ WebAuthn + Backup codes
✅ TOTP + SMS (только если нужна мобильность)

2. Никогда не передавайте OTP по SMS

❌ SMS + Email (обе в open text)
✅ TOTP (зашифровано в приложении)

3. Установите TTL на OTP

✅ TOTP: 30 сек
✅ SMS OTP: 5-10 минут
✅ Email OTP: 15 минут
❌ OTP без TTL (может быть использован позже)

4. Логируйте попытки входа

class LoginAttempt(Base):
    __tablename__ = 'login_attempts'
    
    id = Column(UUID, primary_key=True)
    user_id = Column(UUID)
    ip_address = Column(String(45))
    device_info = Column(JSON)
    mfa_used = Column(Boolean)
    success = Column(Boolean)
    created_at = Column(DateTime)

5. Требуйте MFA для критичных операций

✅ Вход в аккаунт → MFA
✅ Изменение пароля → MFA
✅ Добавление платежного метода → MFA
✅ Удаление аккаунта → MFA

Вывод

Мультифакторная аутентификация — это комбинация:

  1. Фактор 1: Пароль (что ты знаешь)
  2. Фактор 2: TOTP/Push/SMS (что ты имеешь)
  3. Фактор 3: Биометрия (что ты являешься)

Рекомендуемая реализация:

  • TOTP как основной MFA (безопасно, удобно)
  • Backup codes для восстановления
  • WebAuthn для критичных операций
  • Логирование всех попыток входа
  • Защита от фишинга и перехвата