Files
ipam_control/docs/changes/037-totp-2fa/SUMMARY.md
T
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

5.7 KiB
Raw Blame History

Итог: двухфакторная аутентификация TOTP по выбору пользователя (изменение 037)

План: PLAN.md. Пользователь сам включает второй фактор: приложение-аутентификатор (TOTP), настройка по QR. Выдаются 10 одноразовых кодов восстановления; superadmin может сбросить 2FA другому пользователю. Без 2FA вход прежний.

Что сделано

Где Изменение
app/models.py, alembic/versions/0015_user_totp.py users.totp_secret_enc, totp_pending_enc, totp_enabled_at, totp_last_step; таблица recovery_codes (sha256, ON DELETE CASCADE)
app/totp.py (новый) Fernet-шифрование секрета (TOTP_ENC_KEY), генерация секрета и otpauth://, QR в SVG на сервере (segno, data URI), проверка кода ±1 шаг со строгим ростом шага (защита от повторного использования), коды восстановления
app/config.py, docker-compose.yml, scripts/gen_env.py, .env.example TOTP_ENC_KEY: некорректный ключ — отказ старта; без ключа приложение стартует, 2FA недоступна (503)
app/security.py mfa_token (typ="mfa", 5 мин); current_user отклоняет токены с typ
app/api/v1/auth.py Вход в два шага: POST /auth/login → mfa_required + mfa_token; POST /auth/login/2fa — TOTP или код восстановления под теми же advisory-lock и лимитами, что пароль. Общие хелперы _lock_pair, _record_failure, _complete_login
app/api/v1/users.py POST /users/me/2fa/setup (пароль), /enable (код → 10 кодов восстановления), /disable (пароль + код, с лимитами перебора), POST /users/{id}/2fa/reset (superadmin, не себе). Журнал: user.2fa_enabled/disabled/reset, session.2fa_failed
app/schemas.py TokenOut (mfa_required, mfa_token), UserOut (totp_enabled, totp_enabled_at), схемы 2FA
web/app.js Второй шаг входа; пункт «Двухфакторная аутентификация» в меню логина (включение с QR, коды восстановления с копированием, отключение); бейдж 2FA и «Сбросить 2FA» в «Пользователях»; submitDialog не закрывает окно, открытое следующим шагом цепочки
requirements.txt, requirements.lock pyotp, segno, cryptography
tests/test_users.py test_totp_flow: setup/enable, вход с кодом, повтор кода → 401, код восстановления однократен, сброс superadmin
README.md Вход, конфигурация TOTP_ENC_KEY, API, миграции 0001–0015, строка 037

Найдено и исправлено в ходе цикла

  • Ревью — повторное использование кода: login_2fa читал пользователя до advisory-lock; два параллельных запроса с одним кодом проходили оба. Исправлено db.refresh(user) после блокировки; на стенде из двух одновременных запросов — 200 и 401.
  • Ревью — перебор кода при отключении: /users/me/2fa/disable не ограничивал попытки кода (угроза при украденных токене и пароле). Подключены лимиты входа; на стенде 4-я неверная попытка — 429.
  • Ручная проверка UI — обрыв настройки: submitDialog безусловно закрывал окно после шага, закрывая уже открытое следующее окно цепочки (после «Далее» диалог исчезал; на шаге подтверждения закрылось бы окно с кодами восстановления). Исправлено: окно закрывается, только если обработчик не подменил S.dialog. Урок: у UI нет автотестов, и ревью было сосредоточено на серверной части — цепочки диалогов проверяются только вручную.

Проверки

  • pytest -q — 16 passed. Миграция 0015 применена, alembic check чисто, откат до 0014 и повторный upgrade — без ошибок.
  • На стенде: секрет в БД зашифрован (открытого base32 нет); mfa_token на /auth/me → 401; параллельный повтор кода отклонён; лимит перебора кода при отключении срабатывает; события 2FA в журнале. Без ключа — старт разрешён, с неверным ключом — отказ.
  • Ручная проверка UI — пользователь.

Эксплуатация

  • TOTP_ENC_KEY стенда сгенерирован и записан в .env. Ключ нужно хранить в резервной копии: его потеря = сброс 2FA всем пользователям.
  • Пересборка стенда — с --force-recreate (см. изменение 036).