Skip to main content

MFA enrolment problems

Invalid codes, clock drift, already-enrolled errors and the two endpoints people confuse — plus what to do when a phone is lost and recovery codes are not the answer.

Written for whoever runs IT9 min readUpdated

Multi-factor authentication means proving who you are with something beyond a password — here, a six-digit code from an authenticator app on a phone. This article covers the four ways enrolment goes wrong, the one that is genuinely our fault, and the question that brings most of these tickets in: what happens when somebody loses the phone.

How enrolment actually works

Three calls, in order, and every one of them needs the person to be already signed in. Enrolment is something you do from inside a session, not during one.

  1. Start enrolment

    POST /api/v1/auth/mfa/enroll generates a secret, stores it immediately with the enabled flag still off, and returns {"secret": "...", "qr_code_uri": "otpauth://..."}. Because the secret is written at this point, a half-finished enrolment leaves a secret on the account with MFA still inactive.

  2. Add the secret to an authenticator

    The field named qr_code_uri carries an otpauth:// URI, not an image. We do not generate a QR code — rendering one from that URI is the job of whatever interface you are using. If you are scripting against the API, expect a string beginning otpauth://totp/Qorionix: and render it yourself, or have the person type the secret value in manually.

  3. Confirm with a code

    POST /api/v1/auth/mfa/enroll/verify with {"totp_code":"123456"}. Only when this succeeds is the enabled flag set. The audit log records mfa.enabled, or mfa.enroll_verify_failed if the code was wrong.

Turning it back off also needs a valid current code — DELETE /api/v1/auth/mfa with {"totp_code":"..."}. A person who can no longer produce a code therefore cannot disable their own second factor, which is the whole reason the administrator reset below exists.

The two endpoints everyone confuses

There are two verify endpoints and they are not interchangeable. This is the single most common cause of a stuck integration, and one of our own guides had it wrong for a long time.

The enrolment verify endpoint and the login verify endpoint, side by side. They differ in every column.
`/auth/mfa/enroll/verify``/auth/mfa/verify`
PurposeConfirm enrolment and switch MFA onFinish a sign-in that was interrupted by MFA
AuthenticationA normal session bearer tokenThe mfa_token from the sign-in response
Request body{"totp_code":"..."}{"mfa_token":"...","totp_code":"..."}
On success200, no tokens200 with a full token pair
A bad code returns400 invalid TOTP code401 invalid MFA code

The same underlying failure — a code that does not verify — therefore produces two different statuses and two different sentences depending on which endpoint you hit. That inconsistency is ours, and it is worth knowing about before you write a client that branches on the status code.

The code is rejected

You will see 400 invalid TOTP code during enrolment or when disabling, and 401 invalid MFA code during sign-in. Work through these in order.

Why a six-digit code is rejected, most likely cause first.
CauseHow to checkFix
Clock drift on the phoneCompare the phone's clock with a time source. Codes are valid for 30 seconds, and we accept one step either side — so a code is good across a 90-second span, and anything beyond roughly 45 seconds of drift will fail every time.Turn on automatic date and time on the device. Most authenticator apps also have a 'sync time' option.
The code was typed after it expiredThe person types the code as the digits roll over.Wait for a fresh code and enter it promptly. This is common on slow VPN connections.
The secret was added to the wrong account entryThe person has several Qorionix entries in their authenticator from repeated attempts.Delete the duplicates, reset the enrolment, and enrol once cleanly.
A stale enrolmentEnrolment was started, abandoned, and started again — the app still holds the first secret.Start enrolment again and re-scan. The newest secret is the one on the account.
The algorithm was overridden in the appThe otpauth:// URI we emit carries no algorithm parameter, so authenticators fall back to SHA-1, which is what we implement. An app configured manually for SHA-256 will produce codes that never match.Re-add the account from the URI rather than by hand.

For completeness, the parameters are: six digits, a thirty-second period, HMAC-SHA1, a twenty-byte secret, and one step of tolerance either side. If you are validating our behaviour against a reference implementation, those are the numbers to use.

The other enrolment errors

`MFA is already enabled` (409)
The account already has a confirmed second factor. Enrolling again is refused outright rather than silently replacing the existing secret — otherwise a second enrolment would quietly invalidate the authenticator the person is still using. To move to a new phone, disable first (with a valid current code) and enrol again, or have an administrator reset it.
`MFA enrollment not started`
The confirmation step was called without a secret on the account. Usually a client that skipped POST /auth/mfa/enroll, or a session that changed between the two calls. Start again from step one.
`totp_code is required` (400)
The field was absent or empty. Note the field name is totp_code, not code or otp — a mismatch here is a silent-looking failure in a hand-written client.
`MFA is not enabled` (400)
Returned when disabling a factor that was never confirmed. Harmless; the account has no second factor to remove.
`failed to start MFA enrollment` / `MFA verification failed` / `failed to disable MFA` (500)
Ours, not yours. Retry once, then send us the timestamp and the account.

Somebody lost their phone

This is the case worth being deliberate about, because it is where the reset flow above actually gets used: the person cannot sign in, cannot disable their own factor, and either never saved a recovery code or has used up the ones they had. Somebody else has to clear the enrolment for them.

  1. Confirm it is really them

    This step is not paperwork. Clearing a second factor is exactly the action an attacker wants your helpdesk to perform, and a convincing phone call is the cheapest attack there is. Verify out of band — a video call, a known colleague vouching in person, whatever your organisation can actually sustain. Write down what you accepted as proof.

  2. Clear the enrolment

    An administrator calls DELETE /api/v1/organizations/{orgID}/users/{userID}/mfa. It requires the users:write permission, so in practice the organisation's administrator role or a custom role that holds it — see Who can see and do what. The action is recorded in the audit log as mfa.admin_disabled against the administrator who performed it.

  3. Have them enrol again the same day

    Between the reset and the re-enrolment the account is protected by a password alone. Do not leave that gap open overnight. If your sign-on policy demands a second factor, the person will be prompted at their next sign-in anyway.

  4. Consider a passkey instead

    A passkey held on a laptop as well as a phone survives losing either one. It is also the only factor our administrator-hardening policy rule accepts. See Passkey registration fails and Passkeys and security keys.

What we do not support

Being plain about this saves you looking for a setting that is not there. There are exactly two factor types: a TOTP authenticator app, and a passkey or security key. SMS codes, emailed codes and push-approval prompts are not implemented — not stubbed, not behind a flag, simply not present. If a questionnaire asks whether you can require SMS, the answer is no, and the honest follow-up is that SMS is the weakest of the common second factors anyway.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in troubleshooting