From 404c23834ab7e4d83e3bbf3d1ea606f7dbfff53b Mon Sep 17 00:00:00 2001 From: Liam Neville Date: Fri, 22 May 2026 14:40:40 +0300 Subject: [PATCH] fix(login): redirect to email verification when forced MFA has no visible factors (#12060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved When MFA is enforced with only Email OTP as a second factor, new users hit a dead end on the MFA setup screen. The `ChooseSecondFactorToSetup` component hides Email OTP when `emailVerified` is `false`, and if email verification was skipped earlier in the flow (e.g. `EMAIL_VERIFICATION` env not set to `"true"`), no MFA options are visible and the user cannot proceed. # How the Problems Are Solved In the MFA setup server component (`mfa/set/page.tsx`), before rendering, detect when forced MFA has no visible second factors because all configured factors require verification the user hasn't completed. When Email OTP is configured but the user's email is unverified, redirect to `/verify` with `send=true` so a verification code is sent immediately. After the user verifies their email, the existing flow in `verify.ts` calls `checkMFAFactors` which redirects back to `/mfa/set` — where Email OTP is now visible since `emailVerified` is `true`. The redirect pattern follows the existing precedent in `authenticator/set/page.tsx`. # Additional Changes None # Additional Context - Closes #11536 --------- Co-authored-by: Max Peintner --- apps/login/src/app/(login)/mfa/set/page.tsx | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index 1b193a46c..2fd9b5e20 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -16,9 +16,11 @@ import { } from "@/lib/zitadel"; import { Timestamp, timestampDate } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { SecondFactorType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { Metadata } from "next"; import { getTranslations } from "next-intl/server"; import { headers } from "next/headers"; +import { redirect } from "next/navigation"; export async function generateMetadata(): Promise { const t = await getTranslations("mfa"); @@ -111,6 +113,41 @@ export default async function Page(props: { searchParams: Promise { + switch (f) { + case SecondFactorType.OTP: + case SecondFactorType.U2F: + return true; + case SecondFactorType.OTP_EMAIL: + return emailVerified; + case SecondFactorType.OTP_SMS: + return phoneVerified; + default: + return false; + } + }); + + if (!hasVisibleFactor && !emailVerified && loginSettings.secondFactors.includes(SecondFactorType.OTP_EMAIL)) { + const verifyParams = new URLSearchParams({ + loginName: sessionWithData.factors.user.loginName, + send: "true", + }); + const org = organization ?? sessionWithData.factors.user.organizationId; + + if (requestId) { + verifyParams.set("requestId", requestId); + } + + if (org) { + verifyParams.set("organization", org as string); + } + redirect(`/verify?${verifyParams}`); + } + } + return (