Как работает мультифакторная аутентификация?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Мультифакторная аутентификация (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: Пароль (что ты знаешь)
- Фактор 2: TOTP/Push/SMS (что ты имеешь)
- Фактор 3: Биометрия (что ты являешься)
Рекомендуемая реализация:
- TOTP как основной MFA (безопасно, удобно)
- Backup codes для восстановления
- WebAuthn для критичных операций
- Логирование всех попыток входа
- Защита от фишинга и перехвата