Repository navigation
[api-gaps] G13. Enforce MFA for staff, and step-up for sensitive operations #50
Description
Priority 3 · Size M · Console milestone M4
Problem
TOTP is optional for everyone, including the protected superadmin and every holder of users.manage, roles.manage or api_keys.manage. A stolen staff password without MFA is full platform control, and destructive admin actions need no recent re-authentication.
Recommendation
Enforce MFA for staff (never blocking sign-in, so nobody is locked out, including the protected superadmin), and require a fresh MFA step for a short list of sensitive operations.
Plan
- Staff MFA:
- "Staff" = a User holding any platform Role with at least one permission (the same definition as G4 ([api-gaps] G4. Higher read rate limit for staff sessions #41)).
- A staff User without TOTP can sign in, but the session is marked
mfa_enrollment_required. That session may call only self-service endpoints (/users/mereads, MFA enrollment and confirm, password change, logout), and every endpoint needing a platform permission returns403 mfa_enrollment_required. - Assigning a staff Role to a User without MFA is allowed; the Role takes effect once they enroll. The assignment response and the User object say so (
mfa_enrollment_required: true).
- Step-up:
- Sessions record
mfa_verified_at(set at MFA login or re-authentication). - These operations require it within 10 minutes, or return
403 reauthentication_required: creating platform-scoped API Keys; assigning or removing platform Roles;PATCH /platform/settings(G11 ([api-gaps] G11. Runtime platform settings and client feature flags #48)); deleting, suspending or erasing Users;PATCHingrate_limit_per_minute(G12 ([api-gaps] G12. Per-key rate-limit overrides #49)). - New
POST /auth/reauthenticate(TOTP or recovery code) refreshesmfa_verified_at.
- Sessions record
- Tokens: access tokens carry
amr(already there) andmfa_verified_at, so the console can prompt before calling instead of after. - Migration:
sessions.mfa_verified_at timestamptz null. - Rollout: ships behind a 7-day grace period announced in the Changelog; after it, enforcement is on. The protected superadmin must enroll during the grace period.
- Spec: Auth → MFA, Roles & Permissions, NFR → Authentication,
openapi.yaml(error codes, the new endpoint, token claims), Changelog. - Tests: enrollment gating, the self-service allow-list, step-up timing with a test clock, reauthentication with TOTP and with a recovery code.
Acceptance
No staff permission works without MFA, and sensitive operations need an MFA check within the last 10 minutes.
How to build it (per plan-remaining.md → Rules for every change): spec page and openapi.yaml first, then make gen; permission checks from the first commit; contract tests for every new or changed response; expand-only migrations, numbered in the order they reach production (0005 is reserved for #30); a Changelog entry.
Source: the management-console gap analysis (2026-10-21), openapi.yaml 1.0.0-draft.3, 118 operations.
Activity
Adron commentedon Oct 22, 2026
Phishing-resistant MFA (passkeys / WebAuthn) is written up in #65 ([api-gaps] G21). Once it ships, staff MFA here is satisfied by either TOTP or a passkey, and
POST /v1/auth/reauthenticateaccepts both.