diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx
index a8f16f23f2..498b063428 100644
--- a/apps/login/src/app/(login)/mfa/set/page.tsx
+++ b/apps/login/src/app/(login)/mfa/set/page.tsx
@@ -49,10 +49,10 @@ export default async function Page(props: {
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const sessionWithData = sessionId
- ? await loadSessionById(serviceUrl, sessionId, organization)
- : await loadSessionByLoginname(serviceUrl, loginName, organization);
+ ? await loadSessionById(sessionId, organization)
+ : await loadSessionByLoginname(loginName, organization);
- async function getAuthMethodsAndUser(host: string, session?: Session) {
+ async function getAuthMethodsAndUser(session?: Session) {
const userId = session?.factors?.user?.id;
if (!userId) {
@@ -80,7 +80,6 @@ export default async function Page(props: {
}
async function loadSessionByLoginname(
- host: string,
loginName?: string,
organization?: string,
) {
@@ -92,23 +91,18 @@ export default async function Page(props: {
organization,
},
}).then((session) => {
- return getAuthMethodsAndUser(serviceUrl, session);
+ return getAuthMethodsAndUser(session);
});
}
- async function loadSessionById(
- host: string,
- sessionId: string,
- organization?: string,
- ) {
+ async function loadSessionById(sessionId: string, organization?: string) {
const recent = await getSessionCookieById({ sessionId, organization });
return getSession({
serviceUrl,
-
sessionId: recent.id,
sessionToken: recent.token,
}).then((sessionResponse) => {
- return getAuthMethodsAndUser(serviceUrl, sessionResponse.session);
+ return getAuthMethodsAndUser(sessionResponse.session);
});
}
@@ -147,8 +141,10 @@ export default async function Page(props: {
{isSessionValid(sessionWithData).valid &&
loginSettings &&
- sessionWithData && (
+ sessionWithData &&
+ sessionWithData.factors?.user?.id && (
)}
- {force !== "true" && (
-
diff --git a/apps/login/src/components/choose-second-factor-to-setup.tsx b/apps/login/src/components/choose-second-factor-to-setup.tsx
index 21f7aff8a6..e56379e147 100644
--- a/apps/login/src/components/choose-second-factor-to-setup.tsx
+++ b/apps/login/src/components/choose-second-factor-to-setup.tsx
@@ -1,13 +1,17 @@
"use client";
+import { skipMFAAndContinueWithNextUrl } from "@/lib/server/session";
import {
LoginSettings,
SecondFactorType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
+import { useTranslations } from "next-intl";
+import { useRouter } from "next/navigation";
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
type Props = {
+ userId: string;
loginName?: string;
sessionId?: string;
requestId?: string;
@@ -17,9 +21,11 @@ type Props = {
checkAfter: boolean;
phoneVerified: boolean;
emailVerified: boolean;
+ force: boolean;
};
export function ChooseSecondFactorToSetup({
+ userId,
loginName,
sessionId,
requestId,
@@ -29,7 +35,10 @@ export function ChooseSecondFactorToSetup({
checkAfter,
phoneVerified,
emailVerified,
+ force,
}: Props) {
+ const t = useTranslations("mfa");
+ const router = useRouter();
const params = new URLSearchParams({});
if (loginName) {
@@ -49,39 +58,63 @@ export function ChooseSecondFactorToSetup({
}
return (
-
- {loginSettings.secondFactors.map((factor) => {
- switch (factor) {
- case SecondFactorType.OTP:
- return TOTP(
- userMethods.includes(AuthenticationMethodType.TOTP),
- "/otp/time-based/set?" + params,
- );
- case SecondFactorType.U2F:
- return U2F(
- userMethods.includes(AuthenticationMethodType.U2F),
- "/u2f/set?" + params,
- );
- case SecondFactorType.OTP_EMAIL:
- return (
- emailVerified &&
- EMAIL(
- userMethods.includes(AuthenticationMethodType.OTP_EMAIL),
- "/otp/email/set?" + params,
- )
- );
- case SecondFactorType.OTP_SMS:
- return (
- phoneVerified &&
- SMS(
- userMethods.includes(AuthenticationMethodType.OTP_SMS),
- "/otp/sms/set?" + params,
- )
- );
- default:
- return null;
- }
- })}
-
+ <>
+
+ {loginSettings.secondFactors.map((factor) => {
+ switch (factor) {
+ case SecondFactorType.OTP:
+ return TOTP(
+ userMethods.includes(AuthenticationMethodType.TOTP),
+ "/otp/time-based/set?" + params,
+ );
+ case SecondFactorType.U2F:
+ return U2F(
+ userMethods.includes(AuthenticationMethodType.U2F),
+ "/u2f/set?" + params,
+ );
+ case SecondFactorType.OTP_EMAIL:
+ return (
+ emailVerified &&
+ EMAIL(
+ userMethods.includes(AuthenticationMethodType.OTP_EMAIL),
+ "/otp/email/set?" + params,
+ )
+ );
+ case SecondFactorType.OTP_SMS:
+ return (
+ phoneVerified &&
+ SMS(
+ userMethods.includes(AuthenticationMethodType.OTP_SMS),
+ "/otp/sms/set?" + params,
+ )
+ );
+ default:
+ return null;
+ }
+ })}
+
+ {!force && (
+
+ )}
+ >
);
}
diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts
index 7ba37011ad..440a3290eb 100644
--- a/apps/login/src/lib/server/session.ts
+++ b/apps/login/src/lib/server/session.ts
@@ -4,6 +4,7 @@ import { setSessionAndUpdateCookie } from "@/lib/server/cookie";
import {
deleteSession,
getLoginSettings,
+ humanMFAInitSkipped,
listAuthenticationMethodTypes,
} from "@/lib/zitadel";
import { Duration } from "@zitadel/client";
@@ -20,6 +21,53 @@ import {
} from "../cookies";
import { getServiceUrlFromHeaders } from "../service";
+export async function skipMFAAndContinueWithNextUrl({
+ userId,
+ requestId,
+ loginName,
+ sessionId,
+ organization,
+}: {
+ userId: string;
+ loginName?: string;
+ sessionId?: string;
+ requestId?: string;
+ organization?: string;
+}) {
+ const _headers = await headers();
+ const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+
+ const loginSettings = await getLoginSettings({
+ serviceUrl,
+ organization: organization,
+ });
+
+ const skip = await humanMFAInitSkipped({ serviceUrl, userId });
+
+ const url =
+ requestId && sessionId
+ ? await getNextUrl(
+ {
+ sessionId: sessionId,
+ requestId: requestId,
+ organization: organization,
+ },
+ loginSettings?.defaultRedirectUri,
+ )
+ : loginName
+ ? await getNextUrl(
+ {
+ loginName: loginName,
+ organization: organization,
+ },
+ loginSettings?.defaultRedirectUri,
+ )
+ : null;
+ if (url) {
+ return { redirect: url };
+ }
+}
+
export async function continueWithSession({
requestId,
...session
diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts
index 41a537cde1..704d7bbef6 100644
--- a/apps/login/src/lib/verify-helper.ts
+++ b/apps/login/src/lib/verify-helper.ts
@@ -199,18 +199,18 @@ export async function checkMFAFactors(
!availableMultiFactors.length &&
session?.factors?.user?.id
) {
- const user = await getUserByID({
+ const userResponse = await getUserByID({
serviceUrl,
userId: session.factors?.user?.id,
});
- if (
- user.user?.type?.case === "human" &&
- user.user?.type?.value.mfaInitSkipped
- ) {
- const mfaInitSkippedTimestamp = timestampDate(
- user.user.type.value.mfaInitSkipped,
- );
+ const humanUser =
+ userResponse?.user?.type.case === "human"
+ ? userResponse?.user.type.value
+ : undefined;
+
+ if (humanUser?.mfaInitSkipped) {
+ const mfaInitSkippedTimestamp = timestampDate(humanUser.mfaInitSkipped);
const mfaInitSkipLifetimeMillis =
Number(loginSettings.mfaInitSkipLifetime.seconds) * 1000 +
@@ -219,27 +219,32 @@ export async function checkMFAFactors(
const mfaInitSkippedTime = mfaInitSkippedTimestamp.getTime();
const timeDifference = currentTime - mfaInitSkippedTime;
- if (timeDifference > mfaInitSkipLifetimeMillis) {
- const params = new URLSearchParams({
- loginName: session.factors?.user?.loginName as string,
- force: "false", // this defines if the mfa is not forced in the settings and can be skipped
- checkAfter: "true", // this defines if the check is directly made after the setup
- });
-
- if (requestId) {
- params.append("requestId", requestId);
- }
-
- if (organization || session.factors?.user?.organizationId) {
- params.append(
- "organization",
- organization ?? (session.factors?.user?.organizationId as string),
- );
- }
-
- // TODO: provide a way to setup passkeys on mfa page?
- return { redirect: `/mfa/set?` + params };
+ if (!(timeDifference > mfaInitSkipLifetimeMillis)) {
+ // if the time difference is smaller than the lifetime, skip the mfa setup
+ return;
}
}
+
+ // the user has never skipped the mfa init but we have a setting so we redirect
+
+ const params = new URLSearchParams({
+ loginName: session.factors?.user?.loginName as string,
+ force: "false", // this defines if the mfa is not forced in the settings and can be skipped
+ checkAfter: "true", // this defines if the check is directly made after the setup
+ });
+
+ if (requestId) {
+ params.append("requestId", requestId);
+ }
+
+ if (organization || session.factors?.user?.organizationId) {
+ params.append(
+ "organization",
+ organization ?? (session.factors?.user?.organizationId as string),
+ );
+ }
+
+ // TODO: provide a way to setup passkeys on mfa page?
+ return { redirect: `/mfa/set?` + params };
}
}
diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts
index 77d656beda..5e3fdec323 100644
--- a/apps/login/src/lib/zitadel.ts
+++ b/apps/login/src/lib/zitadel.ts
@@ -457,6 +457,21 @@ export async function getUserByID({
return userService.getUserByID({ userId }, {});
}
+export async function humanMFAInitSkipped({
+ serviceUrl,
+ userId,
+}: {
+ serviceUrl: string;
+ userId: string;
+}) {
+ const userService: Client
= await createServiceForHost(
+ UserService,
+ serviceUrl,
+ );
+
+ return userService.humanMFAInitSkipped({ userId }, {});
+}
+
export async function verifyInviteCode({
serviceUrl,
userId,