Skip to content

Handling mfa_required gracefully

loginWithRedirect() or the credentials sign-in returns / throws with error: mfa_required. Your app catches it but doesn't know what to do next. The user is stuck.

Your code expects sign-in to be a single round trip (credentials in, session out). MFA flows are two-round-trip: credentials in → "factor required" out → factor in → session out.

The IntelliAuth SDK handles the second leg if you let it. If your code is short-circuiting the error, the user is stranded.

Don't catch mfa_required as a generic error. Let the SDK present the MFA prompt:

const { loginWithRedirect } = useIntelliAuth()
async function trySignIn() {
try {
await loginWithRedirect()
// success — the SDK rendered MFA inline if needed
} catch (e) {
if (e instanceof IntelliAuthError && e.code === 'session_expired') {
// ...
}
// mfa_required does NOT throw with the SDK's default flow handling
}
}

With the default flow, the SDK renders any required MFA step in-flow. Your code only sees the final outcome.

Read the flow_id and available_factors from the response. Show a factor picker. Submit the chosen factor:

const initial = await authClient.loginWithPassword({ email, password })
if (initial.kind === 'mfa_required') {
const factor = await userPicksFactor(initial.available_factors)
// Initiate the step (sends SMS, returns challenge for WebAuthn, no-op for TOTP)
const step = await authClient.initiateMfaStep(initial.flow_id, factor.kind)
// Get the user's input
const code = await userEntersCode(step)
// Complete
const session = await authClient.completeMfaStep(initial.flow_id, factor.kind, { code })
return session
}

This is what the SDK does internally; do it yourself only if you're building a non-standard MFA UX (e.g., embedding the prompts inside your own modal styling).

Two options:

  • Override the prompt components (the React SDK exposes slot props for this — mfaWebAuthnPrompt, mfaTotpPrompt, etc.). Your component receives the necessary callbacks; the SDK still owns the flow.
  • Drive MFA manually as above.

The first is usually less work for a brand-styling change; the second for a UX rethink.

  • Calling loginWithRedirect() twice on mfa_required — that aborts the in-progress flow and starts a new one. The user has to re-enter credentials and then re-do MFA. Don't.
  • Showing a generic "sign-in failed" message — frustrates users who legitimately have MFA. Branch on mfa_required and show the MFA UI instead.
  • Ignoring the available_factors array — your UI offers all of webauthn / totp / sms but the user has only WebAuthn enrolled. The platform refuses any other factor; the UI is wrong.

The user has no factors enrolled but MFA is required

Section titled “The user has no factors enrolled but MFA is required”

The tenant's policy says MFA is required, but the user signed up before that policy was enacted and never enrolled a factor. The platform's response is mfa_enrolment_required (not mfa_required) — the next step is to surface enrolment UI, then complete sign-in.

if (initial.kind === 'mfa_enrolment_required') {
await userEnrolFactor(initial.flow_id, initial.allowed_factors)
// The platform automatically completes sign-in after the first factor enrols.
}

This first-time enrolment is a teachable moment — explain why MFA is required and what's about to happen. Don't dump them into a TOTP QR with no context.