POST /v1/auth/mfa/setup
Begin TOTP enrollment. Generates a fresh secret, persists it on the user record (marked not-yet-enabled), and returns it plus a QR code PNG for the user to scan into an authenticator app.
::: tip Auth Required: cookie or Bearer. :::
Request
POST /v1/auth/mfa/setup
| Header | Required | Notes |
|---|---|---|
Cookie: identsphere_at=... OR Authorization: Bearer ... | yes | — |
No request body.
Response
200 OK
{
"secret": "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP",
"qr_code_base64": "iVBORw0KGgoAAAANSUhEUgAA..."
}
| Field | Type | Notes |
|---|---|---|
secret | string | Base32 (RFC 4648, no padding), 20 bytes of entropy. Suitable for manual entry into an authenticator app. |
qr_code_base64 | string | PNG image of an otpauth:// URI, base64-encoded. Render with <img src="data:image/png;base64,...">. |
Error responses
| Status | Code | When |
|---|---|---|
| 401 | authentication_required | No valid auth credential. |
| 404 | not_found | User no longer exists. |
| 409 | conflict | MFA is already enabled — use /disable first if rotating. |
| 500 | internal_error | TOTP build / QR generation failure. |
Example: curl
curl -X POST https://auth.example.com/v1/auth/mfa/setup -b cookies.txt
Example: TypeScript (@identsphere/react)
import { useMfaSetup } from '@identsphere/react';
function MfaEnrollStep1() {
const setup = useMfaSetup();
return (
<button
onClick={async () => {
const { qr_code_base64, secret } = await setup.mutateAsync();
setQr(qr_code_base64);
setSecret(secret);
}}
>
Begin MFA enrollment
</button>
);
}
Notes
- The secret is persisted immediately, but
mfa_enabledstaysfalseuntil the user proves they scanned it correctly viaPOST /v1/auth/mfa/enable. - Calling
/setupmore than once before/enableis allowed — each call generates a fresh secret and overwrites the prior pending one. - The TOTP parameters are RFC 6238 defaults: SHA-1, 6 digits, 30-second window. These match what authenticator apps (Google Authenticator, Authy, 1Password, Bitwarden, etc.) expect by default.
- The
issuerbaked into the QR-code URI isAppConfig::app_name, which is what the authenticator displays as the account label.