From 6e6e4218c98b8da4a913619ed371e56652e14821 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 26 Apr 2026 14:43:55 +0200 Subject: [PATCH] fix: skip MFA binding prompt for UserControlled policy during sign-in Logto returns 422 with an MFA recommendation when policy is UserControlled. Call POST /profile/mfa/mfa-skipped to skip the binding prompt, then re-submit. Users who already have MFA enrolled still get the TOTP verification flow. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/sign-in/src/experience-api.ts | 40 +++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/ui/sign-in/src/experience-api.ts b/ui/sign-in/src/experience-api.ts index 7ca5f12..3f2e4bf 100644 --- a/ui/sign-in/src/experience-api.ts +++ b/ui/sign-in/src/experience-api.ts @@ -71,20 +71,42 @@ export class MfaRequiredError extends Error { } } +async function trySubmit(): Promise<{ ok: true; redirectTo: string } | { ok: false; status: number; code: string; message: string }> { + const res = await request('POST', '/submit'); + if (res.ok) { + const data = await res.json(); + return { ok: true, redirectTo: data.redirectTo }; + } + const err = await res.json().catch(() => ({})); + return { ok: false, status: res.status, code: err.code ?? '', message: err.message ?? `Submit failed (${res.status})` }; +} + +async function skipMfaBinding(): Promise { + await request('POST', '/profile/mfa/mfa-skipped'); +} + export async function signIn(identifier: string, password: string): Promise { await initInteraction(); const verificationId = await verifyPassword(identifier, password); await identifyUser(verificationId); - const res = await request('POST', '/submit'); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - if (err.code === 'user.missing_mfa') { - throw new MfaRequiredError(); - } - throw new Error(err.message || `Submit failed (${res.status})`); + const result = await trySubmit(); + if (result.ok) return result.redirectTo; + + // MFA already enrolled — user must verify (show TOTP input) + if (result.code === 'user.missing_mfa' || result.code === 'session.mfa.require_mfa_verification') { + throw new MfaRequiredError(); } - const data = await res.json(); - return data.redirectTo; + + // MFA not enrolled, UserControlled policy — skip the binding prompt. + // Also fallback: any 422 with an MFA-related code we don't recognize — try skip before failing. + if (result.status === 422 && result.code.includes('mfa')) { + await skipMfaBinding(); + const retry = await trySubmit(); + if (retry.ok) return retry.redirectTo; + throw new Error(retry.message); + } + + throw new Error(result.message); } // --- Registration ---