skip button and server action

This commit is contained in:
Max Peintner
2025-02-28 15:22:56 +01:00
parent 546edee64f
commit 83df30e525
5 changed files with 173 additions and 81 deletions

View File

@@ -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 && (
<ChooseSecondFactorToSetup
userId={sessionWithData.factors?.user?.id}
loginName={loginName}
sessionId={sessionId}
requestId={requestId}
@@ -158,15 +154,10 @@ export default async function Page(props: {
phoneVerified={sessionWithData.phoneVerified ?? false}
emailVerified={sessionWithData.emailVerified ?? false}
checkAfter={checkAfter === "true"}
force={force === "true"}
></ChooseSecondFactorToSetup>
)}
{force !== "true" && (
<div>
<p>{t("set.skip")}</p>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<BackButton />
<span className="flex-grow"></span>

View File

@@ -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 (
<div className="grid grid-cols-1 gap-5 w-full pt-4">
{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;
}
})}
</div>
<>
<div className="grid grid-cols-1 gap-5 w-full pt-4">
{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;
}
})}
</div>
{!force && (
<button
className="transition-all text-sm hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={async () => {
const resp = await skipMFAAndContinueWithNextUrl({
userId,
loginName,
sessionId,
organization,
requestId,
});
if (resp?.redirect) {
return router.push(resp.redirect);
}
}}
type="button"
data-testid="reset-button"
>
{t("set.skip")}
</button>
)}
</>
);
}

View File

@@ -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

View File

@@ -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 };
}
}

View File

@@ -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<typeof UserService> = await createServiceForHost(
UserService,
serviceUrl,
);
return userService.humanMFAInitSkipped({ userId }, {});
}
export async function verifyInviteCode({
serviceUrl,
userId,