Problem
Section titled “Problem”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.
Resolution
Section titled “Resolution”The recommended pattern
Section titled “The recommended pattern”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.
If you're driving MFA manually (custom UI)
Section titled “If you're driving MFA manually (custom UI)”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).
If the SDK's default prompts don't match your design
Section titled “If the SDK's default prompts don't match your design”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.
Common mistakes
Section titled “Common mistakes”- Calling
loginWithRedirect()twice onmfa_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_requiredand show the MFA UI instead. - Ignoring the
available_factorsarray — your UI offers all ofwebauthn / totp / smsbut 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.