MFA Enrollment (authenticated)
POST /api/v1/auth/mfa/totp — start enrollment (local and LDAP accounts)
POST /api/v1/auth/mfa/totp/confirm — confirm with the first code, get recovery codes
DELETE /api/v1/auth/mfa/totp — disable your own TOTP
POST /auth/mfa/totp returns {secret, otpauth_uri, qr_png} where qr_png is a server-rendered data:image/png;base64,… (no client QR dependency; the secret never crosses an external chart API). The enrollment is pending until confirm verifies a code and returns one-time recovery_codes (shown once). OIDC/SAML accounts get 400 (they manage MFA at their IdP); a second enrollment while one is confirmed gets 409.
DELETE /auth/mfa/totp requires proof of possession in the body — a session cookie alone is not enough, because a stolen session must not be able to strip the second factor. Send any one of:
{"code": "123456"} // current TOTP code
{"recovery_code": "ABCDE-23456"}
{"password": "…"} // local accounts only — SSO accounts have no local password
A used recovery code is burned even though the record is about to be deleted. No proof gets 400; wrong proof gets 401 and is audited as mfa_disable_failed. The route is rate-limited at 3 requests per minute per IP, like its enrollment siblings. A user who has lost every factor is not locked out — an admin can still reset it.
Admin reset of another user's TOTP: DELETE /api/v1/users/{userID}/mfa (admin-only, audited as mfa_reset).