fix(auth): make admin 2FA optional via OPUS_REQUIRE_ADMIN_MFA flag #23

Merged
elirtf merged 1 commit from fix/admin-mfa-optional into master 2026-10-17 21:48:51 +00:00
Contributor

Problem

After creating the admin account, the user is hard-gated behind a mandatory 2FA enrollment page with no skip option. Every API call returns 403 (mfa_enrollment_required) until 2FA is set up. This contradicts the original design spec (docs/superpowers/specs/2026-09-08-admin-mfa-design.md), which explicitly listed mandatory enforcement as a non-goal:

Mandatory enforcement. Opt-in, self-service. An admin turns it on from Settings when ready.

The security checklist (docs/security-testing-checklist.md:953) also documents it as "opt-in rather than mandatory, which is a documented, deliberate scope decision."

Also fixes a typo: "Enrol" → "Enroll" in the MFA setup page.

Solution

Add OPUS_REQUIRE_ADMIN_MFA env flag (default: false) to control enforcement mode.

When false (default — opt-in, matches original spec)

  • Admin can use the system normally without 2FA
  • A dismissible enrollment nudge appears once per session after login
  • "Skip for now" link with confirmation dialog ("Are you sure? 2FA is strongly recommended")
  • Nudge reappears on next login (per-session dismissal, not permanent)
  • Admin can voluntarily enable 2FA from Settings → Configuration
  • Admin who enrolled can self-disable with their password

When true (mandatory — current behavior preserved)

  • Hard gate: 403 on all routes except the 4 enrollment endpoints
  • No skip option on the MFA setup page
  • Admin cannot self-disable 2FA
  • Identical to today's behavior

Changes

File Change
app/routes/api/utils.py _mfa_enrollment_block() checks OPUS_REQUIRE_ADMIN_MFA
app/routes/api/auth.py go2rtc verify + admin self-disable gated by flag; /me exposes require_admin_mfa
frontend/src/context/AuthContext.jsx mfaDismissed per-session state (resets on login/logout)
frontend/src/components/ProtectedRoute.jsx Hard gate when mandatory; soft dismissible gate when optional
frontend/src/pages/MfaSetup.jsx Typo fix, conditional subtitle, Skip flow with confirmation
frontend/src/components/TwoFactorSettings.jsx canDisable respects the flag for admin self-disable
tests/test_admin_mfa_required.py Mandatory tests set the flag; 5 new tests for optional mode

Testing

  • 637 tests pass (1 pre-existing unrelated monitoring compose test deselected)
  • Existing mandatory-mode tests preserved — they now set OPUS_REQUIRE_ADMIN_MFA=1 via a mandatory_mfa fixture
  • New tests for optional mode: unenrolled admin can access require_auth/require_admin routes, go2rtc verify, /me reports the flag, admin can self-disable
  • Stack rebuilt and verified: frontend bundle contains "Skip for now", "Enroll below" (typo gone), and require_admin_mfa

No migration needed

No DB schema changes, no new routes, no new dependencies.

## Problem After creating the admin account, the user is hard-gated behind a mandatory 2FA enrollment page with no skip option. Every API call returns 403 (`mfa_enrollment_required`) until 2FA is set up. This contradicts the original design spec (`docs/superpowers/specs/2026-09-08-admin-mfa-design.md`), which explicitly listed mandatory enforcement as a **non-goal**: > *Mandatory enforcement.* Opt-in, self-service. An admin turns it on from Settings when ready. The security checklist (`docs/security-testing-checklist.md:953`) also documents it as "opt-in rather than mandatory, which is a documented, deliberate scope decision." Also fixes a typo: "Enrol" → "Enroll" in the MFA setup page. ## Solution Add `OPUS_REQUIRE_ADMIN_MFA` env flag (default: `false`) to control enforcement mode. ### When `false` (default — opt-in, matches original spec) - Admin can use the system normally without 2FA - A dismissible enrollment nudge appears once per session after login - "Skip for now" link with confirmation dialog ("Are you sure? 2FA is strongly recommended") - Nudge reappears on next login (per-session dismissal, not permanent) - Admin can voluntarily enable 2FA from Settings → Configuration - Admin who enrolled can self-disable with their password ### When `true` (mandatory — current behavior preserved) - Hard gate: 403 on all routes except the 4 enrollment endpoints - No skip option on the MFA setup page - Admin cannot self-disable 2FA - Identical to today's behavior ## Changes | File | Change | |---|---| | `app/routes/api/utils.py` | `_mfa_enrollment_block()` checks `OPUS_REQUIRE_ADMIN_MFA` | | `app/routes/api/auth.py` | go2rtc verify + admin self-disable gated by flag; `/me` exposes `require_admin_mfa` | | `frontend/src/context/AuthContext.jsx` | `mfaDismissed` per-session state (resets on login/logout) | | `frontend/src/components/ProtectedRoute.jsx` | Hard gate when mandatory; soft dismissible gate when optional | | `frontend/src/pages/MfaSetup.jsx` | Typo fix, conditional subtitle, Skip flow with confirmation | | `frontend/src/components/TwoFactorSettings.jsx` | `canDisable` respects the flag for admin self-disable | | `tests/test_admin_mfa_required.py` | Mandatory tests set the flag; 5 new tests for optional mode | ## Testing - **637 tests pass** (1 pre-existing unrelated monitoring compose test deselected) - Existing mandatory-mode tests preserved — they now set `OPUS_REQUIRE_ADMIN_MFA=1` via a `mandatory_mfa` fixture - New tests for optional mode: unenrolled admin can access `require_auth`/`require_admin` routes, go2rtc verify, `/me` reports the flag, admin can self-disable - Stack rebuilt and verified: frontend bundle contains "Skip for now", "Enroll below" (typo gone), and `require_admin_mfa` ## No migration needed No DB schema changes, no new routes, no new dependencies.
The original design spec (docs/superpowers/specs/2026-09-08-admin-mfa-design.md)
listed mandatory MFA enforcement as an explicit non-goal — opt-in, self-service.
The implementation contradicted this by hard-gating every admin session behind
2FA enrollment, blocking all API calls and rendering a non-dismissible gate page
right after account creation.

This restores the opt-in intent:

- OPUS_REQUIRE_ADMIN_MFA env flag (default: false) controls the enforcement mode
- When false (default): admin can use the system without 2FA; a dismissible
  enrollment nudge appears once per session with a "Skip for now" confirmation
- When true: current hard-gate behavior (403 on all routes except enrollment
  allowlist, no self-disable, no skip option)
- /api/auth/me exposes require_admin_mfa so the frontend knows the policy
- Admin self-disable (/mfa/disable) is allowed when MFA is optional
- Frontend canDisable in TwoFactorSettings respects the flag
- Typo fix: "Enrol" → "Enroll" in MfaSetup.jsx

Tests: existing mandatory-mode tests now set the flag via fixture; 5 new tests
cover the optional (default) mode — unenrolled admin can access require_auth and
require_admin routes, go2rtc verify, /me reports the flag, and self-disable works.
elirtf merged commit 32c2e01059 into master 2026-10-17 21:48:51 +00:00
elirtf deleted branch fix/admin-mfa-optional 2026-10-17 21:48:51 +00:00
Sign in to join this conversation.
No reviewers
No labels
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
elirtf/opus!23
No description provided.