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" && ( -
-

{t("set.skip")}

-
- )} -
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,