Enroll
Generate a new TOTP secret + QR code. Re-enrolling overwrites any unconfirmed prior secret (idempotent) — mfa_enabled stays False until /enroll/confirm verifies a real code from it.
If the user already has mfa_enabled=True, this is a RE-enrollment (e.g. lost device) and requires the same re-auth bar as /disable (current password + a valid current-factor OTP code) — otherwise a bare session could silently swap the TOTP factor to one only an attacker knows, with no password/OTP ever demanded (the forced mid-login path never hits this branch: login() only mints an enroll-purpose pending token when mfa_enabled is already False).
Authorizations
Request Body required
Enroll request. Either an authenticated session (voluntary, MEMBER/ VIEWER) or an mfa_pending/purpose=enroll token (forced mid-login, OWNER/ADMIN) authorizes this — see app/api/mfa.py for the dual-context dependency.
current_password/otp_code are required ONLY when re-enrolling a user who already has mfa_enabled=True (re-auth bar, same as /disable) — a bare session must not be able to silently swap an already-active TOTP factor to one only the caller knows, which would otherwise let a hijacked session (or an XSS the CSRF header exists to resist) lock the real user out and take over their 2FA with no password/OTP ever demanded.
Responses
200
Successful Response
QR code (base64 PNG) + manual-entry secret. Deliberately omits the otpauth:// URI as a separate field — it just re-encodes the same secret a second time in the clear for no benefit.
object
422
Validation Error