Files
ayurishchevandClaude Opus 5.5 d3495fb077 Задача 037: двухфакторная аутентификация TOTP по выбору пользователя
Пользователь сам включает TOTP (QR, приложение-аутентификатор); 10 одноразовых кодов
восстановления; superadmin сбрасывает 2FA другому пользователю. Вход в два шага:
/auth/login → mfa_token, /auth/login/2fa — под теми же лимитами и advisory-lock, что пароль.
Секрет шифруется Fernet-ключом TOTP_ENC_KEY, защита от повторного использования кода
(totp_last_step), лимиты перебора кода при отключении. Миграция 0015, зависимости pyotp,
segno, cryptography. UI: второй шаг входа, диалоги 2FA в меню логина, бейдж и сброс
в «Пользователях»; submitDialog не закрывает окно, открытое следующим шагом цепочки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:57:14 +03:00

34 lines
2.2 KiB
Python

from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
database_url: str = "postgresql+psycopg://ipam:ipam@localhost:55432/ipam"
jwt_secret: str = "" # обязателен: проверяется при старте приложения (validate_secrets), миграциям не нужен
jwt_ttl_minutes: int = 480
admin_username: str = "admin"
admin_password: str = ""
trusted_proxies: str = "" # CIDR через запятую: от них принимается X-Forwarded-For
totp_enc_key: str = "" # изменение 037: ключ Fernet для шифрования секретов TOTP; пусто — 2FA недоступна (503), потеря ключа = сброс 2FA всем
settings = Settings()
_WEAK_SECRETS = {"change-me", "changeme", "dev-only-secret", "secret", "password"}
def validate_secrets(s: Settings = settings) -> None:
"""Fail-fast при старте: подпись токенов известным/коротким ключом позволяет подделать токен любого пользователя."""
if len(s.jwt_secret) < 32 or s.jwt_secret.lower() in _WEAK_SECRETS:
raise RuntimeError("JWT_SECRET не задан, короче 32 символов или является заглушкой: сгенерируйте значение (python scripts/gen_env.py)")
if s.admin_password and len(s.admin_password) < 8:
raise RuntimeError("ADMIN_PASSWORD короче 8 символов: пароль слабее, чем допускает UI")
if s.totp_enc_key: # изменение 037: не задан — приложение стартует, 2FA недоступна; задан некорректно — отказ старта
from cryptography.fernet import Fernet
try:
Fernet(s.totp_enc_key.encode())
except (ValueError, TypeError) as e:
raise RuntimeError("TOTP_ENC_KEY некорректен: нужен ключ Fernet (python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\")") from e