Пользователь сам включает 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>
40 lines
5.7 KiB
Markdown
40 lines
5.7 KiB
Markdown
# Итог: двухфакторная аутентификация 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).
|