mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-14 04:27:34 +00:00
skip button and server action
This commit is contained in:
@@ -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>
|
||||
|
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user