[SEC-Q2] MFA: добавить verified_at claim в JWT после step-up #1151

Closed
opened 2026-07-19 17:05:42 +00:00 by andrei · 2 comments
Owner

Из SEC-Q2 MEDIUM backlog (аудит, срез 2026-07-10).

После step-up MFA-подтверждения JWT не несёт mfa_verified_at claim — downstream-проверки не могут отличить свежее подтверждение от старой сессии. Добавить claim при выдаче/refresh токена после verifyPassword/MFA step-up, проверять давность в чувствительных операциях (payout, ключевые настройки).

Контекст кода: api/src/lib/adminMfaSession.ts (adminMfaSessionKey), JWT issue path в Auth-oauth модуле.

Из SEC-Q2 MEDIUM backlog (аудит, срез 2026-07-10). После step-up MFA-подтверждения JWT не несёт mfa_verified_at claim — downstream-проверки не могут отличить свежее подтверждение от старой сессии. Добавить claim при выдаче/refresh токена после verifyPassword/MFA step-up, проверять давность в чувствительных операциях (payout, ключевые настройки). Контекст кода: api/src/lib/adminMfaSession.ts (adminMfaSessionKey), JWT issue path в Auth-oauth модуле.
Author
Owner

Уточнение scope по коду (master, анализ 2026-07-20):

Частично уже реализовано механизмом SEC-Q2-2026:

  • api/src/lib/adminMfaSession.ts — session-scoped доказательство прохождения 2FA-challenge, keyed by sessionId, переживает refresh; читается requireAdmin2FA.
  • api/src/services/auth-session/sessions.ts:62-64 — login-path с реальным 2FA ставит mfaVerified=true.

Остаточный гап (то, что просит этот issue и чего механизм НЕ даёт):

  1. Хранится boolean, не verified_at timestamp — точную давность подтверждения узнать нельзя.
  2. adminMfaSession.ts:11-13 — флаг re-extended на каждом refresh (TTL 4h), поэтому через часы активной сессии он всё ещё true → операция «MFA не старше N минут» невыполнима.
  3. Admin money-ops уже перекрыты сильнее — requireFreshAdmin2FA (api/src/lib/adminFreshTotp.ts) требует свежий TOTP-код на КАЖДУЮ операцию. Т.е. остаётся только user-path (напр. withdrawal).

Блокер на реализацию — product/policy decision (не техника): какие именно user-операции требуют свежего MFA и с каким max-age (5 мин? 15?). Без этого реализация = угадывание политики. Технический план после решения: заменить boolean на { verifiedAt: unixSec }, НЕ обновлять verifiedAt на refresh (только на реальном 2FA-challenge), добавить requireRecentMfa(req, maxAgeSec), подключить к выбранным user-операциям + TDD.

**Уточнение scope по коду (master, анализ 2026-07-20):** Частично уже реализовано механизмом SEC-Q2-2026: - `api/src/lib/adminMfaSession.ts` — session-scoped доказательство прохождения 2FA-challenge, keyed by sessionId, переживает refresh; читается `requireAdmin2FA`. - `api/src/services/auth-session/sessions.ts:62-64` — login-path с реальным 2FA ставит `mfaVerified=true`. Остаточный гап (то, что просит этот issue и чего механизм НЕ даёт): 1. Хранится **boolean**, не `verified_at` timestamp — точную давность подтверждения узнать нельзя. 2. `adminMfaSession.ts:11-13` — флаг **re-extended на каждом refresh** (TTL 4h), поэтому через часы активной сессии он всё ещё true → операция «MFA не старше N минут» невыполнима. 3. Admin money-ops уже перекрыты сильнее — `requireFreshAdmin2FA` (`api/src/lib/adminFreshTotp.ts`) требует свежий TOTP-код на КАЖДУЮ операцию. Т.е. остаётся только **user-path** (напр. withdrawal). **Блокер на реализацию — product/policy decision (не техника):** какие именно user-операции требуют свежего MFA и с каким max-age (5 мин? 15?). Без этого реализация = угадывание политики. Технический план после решения: заменить boolean на `{ verifiedAt: unixSec }`, НЕ обновлять verifiedAt на refresh (только на реальном 2FA-challenge), добавить `requireRecentMfa(req, maxAgeSec)`, подключить к выбранным user-операциям + TDD.
Author
Owner

Закрыто как covered-stronger (анализ поверхностей 2026-07-20, evidence из master).

Полная проверка чувствительных операций показала: везде уже стоит per-operation свежий фактор, что СТРОГО СИЛЬНЕЕ предложенного mfa_verified_at claim с временным окном (15 мин). Добавлять verified_at = дублировать слабейшую защиту поверх сильнейшей (L19/L4 — избыточный код на money-path не пишем).

Evidence по поверхностям:

  • Вывод средств (bank+crypto), api/src/services/income-payout/request-bank-payout.ts:109-133: SEC-HIGH-1 — OTP обязателен на КАЖДЫЙ payout (verifyOtp, иначе 401); AUDIT-243 RP-001 — replay-safe TOTP на каждую операцию. Свежий фактор per-op, не окно.
  • Смена пароля, api/src/controllers/auth/profile.ts:208-211: требует currentPassword (re-auth фактором).
  • Disable 2FA, api/src/controllers/auth/twofa.ts:61-71: требует свежий TOTP code.
  • Admin money-ops (mint/escrow/dividend/refund): requireFreshAdmin2FA (api/src/lib/adminFreshTotp.ts) — свежий TOTP на каждую операцию.
  • requireAdmin2FA session-proof: session-scoped mfaVerified (SEC-Q2-2026, api/src/lib/adminMfaSession.ts) уже отличает пройденный challenge от простого enrollment.

Вывод: механизм session-freshness verified_at нигде не даёт дополнительной защиты — все критичные пути используют per-operation step-up. Реального гапа нет. Если в будущем появится чувствительная операция БЕЗ per-op фактора — завести новый узкий issue под неё.

**Закрыто как covered-stronger (анализ поверхностей 2026-07-20, evidence из master).** Полная проверка чувствительных операций показала: везде уже стоит **per-operation свежий фактор**, что СТРОГО СИЛЬНЕЕ предложенного `mfa_verified_at` claim с временным окном (15 мин). Добавлять verified_at = дублировать слабейшую защиту поверх сильнейшей (L19/L4 — избыточный код на money-path не пишем). Evidence по поверхностям: - **Вывод средств** (bank+crypto), `api/src/services/income-payout/request-bank-payout.ts:109-133`: SEC-HIGH-1 — OTP обязателен на КАЖДЫЙ payout (`verifyOtp`, иначе 401); AUDIT-243 RP-001 — replay-safe TOTP на каждую операцию. Свежий фактор per-op, не окно. - **Смена пароля**, `api/src/controllers/auth/profile.ts:208-211`: требует `currentPassword` (re-auth фактором). - **Disable 2FA**, `api/src/controllers/auth/twofa.ts:61-71`: требует свежий TOTP `code`. - **Admin money-ops** (mint/escrow/dividend/refund): `requireFreshAdmin2FA` (`api/src/lib/adminFreshTotp.ts`) — свежий TOTP на каждую операцию. - **requireAdmin2FA session-proof**: session-scoped `mfaVerified` (SEC-Q2-2026, `api/src/lib/adminMfaSession.ts`) уже отличает пройденный challenge от простого enrollment. Вывод: механизм session-freshness verified_at нигде не даёт дополнительной защиты — все критичные пути используют per-operation step-up. Реального гапа нет. Если в будущем появится чувствительная операция БЕЗ per-op фактора — завести новый узкий issue под неё.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
europa-tech-srl/europatech#1151
No description provided.