Skip to content

Enroll

POST
/api/auth/mfa/enroll
curl --request POST \
--url https://example.com/api/auth/mfa/enroll \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "current_password": "example", "mfa_pending_token": "example", "otp_code": "example" }'

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).

Media type application/json
MfaEnrollRequest

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.

object
current_password
Any of:
string
mfa_pending_token
Any of:
string
otp_code
Any of:
string
>= 6 characters <= 6 characters /^\d{6}$/
Example generated
{
"current_password": "example",
"mfa_pending_token": "example",
"otp_code": "example"
}

Successful Response

Media type application/json
MfaEnrollResponse

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
qr_code_png_base64
required
Qr Code Png Base64
string
secret_base32
required
Secret Base32
string
Example generated
{
"qr_code_png_base64": "example",
"secret_base32": "example"
}

Validation Error

Media type application/json
HTTPValidationError
object
detail
Detail
Array<object>
ValidationError
object
ctx
Context
object
input
Input
loc
required
Location
Array
msg
required
Message
string
type
required
Error Type
string
Example generated
{
"detail": [
{
"ctx": {},
"input": "example",
"loc": [
"example"
],
"msg": "example",
"type": "example"
}
]
}