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