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.
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.
Start enrolment
POST /api/v1/auth/mfa/enrollgenerates 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.Add the secret to an authenticator
The field named
qr_code_uricarries anotpauth://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 beginningotpauth://totp/Qorionix:and render it yourself, or have the person type thesecretvalue in manually.Confirm with a code
POST /api/v1/auth/mfa/enroll/verifywith{"totp_code":"123456"}. Only when this succeeds is the enabled flag set. The audit log recordsmfa.enabled, ormfa.enroll_verify_failedif 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.
| `/auth/mfa/enroll/verify` | `/auth/mfa/verify` | |
|---|---|---|
| Purpose | Confirm enrolment and switch MFA on | Finish a sign-in that was interrupted by MFA |
| Authentication | A normal session bearer token | The mfa_token from the sign-in response |
| Request body | {"totp_code":"..."} | {"mfa_token":"...","totp_code":"..."} |
| On success | 200, no tokens | 200 with a full token pair |
| A bad code returns | 400 invalid TOTP code | 401 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.
| Cause | How to check | Fix |
|---|---|---|
| Clock drift on the phone | Compare 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 expired | The 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 entry | The person has several Qorionix entries in their authenticator from repeated attempts. | Delete the duplicates, reset the enrolment, and enrol once cleanly. |
| A stale enrolment | Enrolment 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 app | The 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, notcodeorotp— 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.
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.
Clear the enrolment
An administrator calls
DELETE /api/v1/organizations/{orgID}/users/{userID}/mfa. It requires theusers:writepermission, 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 asmfa.admin_disabledagainst the administrator who performed it.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.
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.
- Multi-factor authentication factors — what each factor is and when a policy rule will accept it.
- Set up two-factor authentication — the first-time walkthrough.
- A user cannot sign in — if the block is happening before the code prompt.