Skip to main content

POST /v1/auth/mfa/enable

Confirm the user can produce valid TOTP codes for the secret minted by /setup. On success, flips mfa_enabled = true and returns 10 fresh recovery codes (shown exactly once).

::: tip Auth Required: cookie or Bearer. :::

Request

POST /v1/auth/mfa/enable

HeaderRequiredNotes
Cookie: identsphere_at=... OR Authorization: Bearer ...yes
Content-Type: application/jsonyes

Body

{ "code": "123456" }
FieldTypeRequiredNotes
codestringyes6–8 chars. The current TOTP code from the user's authenticator.

Response

200 OK

{
"recovery_codes": [
"Yj7nKp2qA8",
"M4xRv9bC1z",
"Q3wPe5sL8h",
"T6yUu2iR4o",
"B7nMx5cV1z",
"L8kJh4gF2d",
"S3aQ5wE9rT",
"F2dG3hY4uI",
"Z1xC4vB7nM",
"P9oL5kJ8hG"
]
}

::: warning Show these exactly once The plaintext codes are returned here and NEVER again. Display them, prompt the user to save them somewhere safe (password manager, printed sheet), and discard from client state. :::

Error responses

StatusCodeWhen
400invalid_inputCode is missing, too short, or too long. MFA setup has not been started (/setup wasn't called first).
401authentication_requiredNo valid auth credential, OR the TOTP code doesn't match. The handler accepts ±1 30-second window of clock skew.
404not_foundUser no longer exists.
409conflictMFA is already enabled.
500internal_errorTOTP build or DB failure.

Example: curl

curl -X POST https://auth.example.com/v1/auth/mfa/enable \
-H 'Content-Type: application/json' \
-b cookies.txt \
-d '{"code":"123456"}'

Example: TypeScript (@identsphere/react)

import { useMfaEnable } from '@identsphere/react';

function MfaEnrollStep2() {
const enable = useMfaEnable();
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const code = new FormData(e.currentTarget).get('code') as string;
const { recovery_codes } = await enable.mutateAsync({ code });
// Show recovery_codes to the user, prompt them to save
}}
>
<input name="code" placeholder="123456" />
<button type="submit">Enable MFA</button>
</form>
);
}

Notes

  • Recovery codes are stored in the database as SHA-256 hashes; the SDK never retains the plaintext after this response.
  • Any prior recovery codes for the user are wiped before the new batch is inserted — there's exactly one valid batch at any time.
  • An audit entry (auth.mfa.enabled) is recorded asynchronously.
  • After enabling, future logins for this user will return status: "mfa_required" and require completion of POST /v1/auth/mfa/challenge.