mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
fix(login): redirect to email verification when forced MFA has no visible factors (#12060)
# 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 <[email protected]>
This commit is contained in:
co-authored by
Max Peintner
parent
b4f2da2171
commit
404c23834a
@@ -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<Metadata> {
|
||||
const t = await getTranslations("mfa");
|
||||
@@ -111,6 +113,41 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
|
||||
const { valid } = sessionWithData ? isSessionValid(sessionWithData) : { valid: false };
|
||||
|
||||
if (force === "true" && valid && sessionWithData?.factors?.user?.loginName && loginSettings) {
|
||||
const emailVerified = sessionWithData.emailVerified ?? false;
|
||||
const phoneVerified = sessionWithData.phoneVerified ?? false;
|
||||
const hasVisibleFactor = loginSettings.secondFactors.some((f) => {
|
||||
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 (
|
||||
<DynamicTheme branding={branding}>
|
||||
<div className="flex flex-col space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user