Skip to content

[api-gaps] G13. Enforce MFA for staff, and step-up for sensitive operations #50

Open

Description

@Adron

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

  1. 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/me reads, MFA enrollment and confirm, password change, logout), and every endpoint needing a platform permission returns 403 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).
  2. Step-up:
  3. Tokens: access tokens carry amr (already there) and mfa_verified_at, so the console can prompt before calling instead of after.
  4. Migration: sessions.mfa_verified_at timestamptz null.
  5. 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.
  6. Spec: Auth → MFA, Roles & Permissions, NFR → Authentication, openapi.yaml (error codes, the new endpoint, token claims), Changelog.
  7. 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

  1. Adron commented on Oct 22, 2026

    @Adron
    MemberAuthor

    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/reauthenticate accepts both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions