mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-25 10:12:51 +00:00
verify commands
This commit is contained in:
@@ -88,12 +88,21 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user && (
|
{sessionFactors ? (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
loginName={user.preferredLoginName}
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
displayName={human?.profile?.displayName}
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
showDropdown={false}
|
showDropdown
|
||||||
/>
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
) : (
|
||||||
|
user && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={user.preferredLoginName}
|
||||||
|
displayName={human?.profile?.displayName}
|
||||||
|
showDropdown={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{id &&
|
{id &&
|
||||||
@@ -110,10 +119,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
// check if auth methods are set
|
// check if auth methods are set
|
||||||
<VerifyForm
|
<VerifyForm
|
||||||
loginName={loginName}
|
loginName={loginName}
|
||||||
|
organization={organization}
|
||||||
userId={id}
|
userId={id}
|
||||||
code={code}
|
code={code}
|
||||||
isInvite={invite === "true"}
|
isInvite={invite === "true"}
|
||||||
params={params}
|
authRequestId={authRequestId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -18,17 +18,19 @@ type Inputs = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
userId: string;
|
userId: string;
|
||||||
loginName?: string;
|
loginName?: string;
|
||||||
|
organization?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
isInvite: boolean;
|
isInvite: boolean;
|
||||||
params: URLSearchParams;
|
authRequestId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VerifyForm({
|
export function VerifyForm({
|
||||||
userId,
|
userId,
|
||||||
loginName,
|
loginName,
|
||||||
|
organization,
|
||||||
|
authRequestId,
|
||||||
code,
|
code,
|
||||||
isInvite,
|
isInvite,
|
||||||
params,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("verify");
|
const t = useTranslations("verify");
|
||||||
|
|
||||||
@@ -74,6 +76,9 @@ export function VerifyForm({
|
|||||||
code: value.code,
|
code: value.code,
|
||||||
userId,
|
userId,
|
||||||
isInvite: isInvite,
|
isInvite: isInvite,
|
||||||
|
loginName: loginName,
|
||||||
|
organization: organization,
|
||||||
|
authRequestId: authRequestId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setError("Could not verify user");
|
setError("Could not verify user");
|
||||||
|
@@ -350,8 +350,9 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
if (command.authRequestId) {
|
if (command.authRequestId) {
|
||||||
params.set("authRequestId", command.authRequestId);
|
params.set("authRequestId", command.authRequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.loginName) {
|
if (command.loginName) {
|
||||||
params.set("loginName", command.loginName);
|
params.set("email", command.loginName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { redirect: "/register?" + params };
|
return { redirect: "/register?" + params };
|
||||||
|
@@ -17,7 +17,6 @@ import {
|
|||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import { createUserServiceClient } from "@zitadel/client/v2";
|
import { createUserServiceClient } from "@zitadel/client/v2";
|
||||||
import { createServerTransport } from "@zitadel/node";
|
import { createServerTransport } from "@zitadel/node";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
|
||||||
import {
|
import {
|
||||||
Checks,
|
Checks,
|
||||||
ChecksSchema,
|
ChecksSchema,
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
||||||
|
import { checkMFAFactors } from "../verify-helper";
|
||||||
|
|
||||||
type ResetPasswordCommand = {
|
type ResetPasswordCommand = {
|
||||||
loginName: string;
|
loginName: string;
|
||||||
@@ -196,7 +196,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { redirect: `/verify` + params };
|
return { redirect: `/verify?` + params };
|
||||||
}
|
}
|
||||||
|
|
||||||
checkMFAFactors(
|
checkMFAFactors(
|
||||||
@@ -341,110 +341,3 @@ export async function checkSessionAndSetPassword({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkMFAFactors(
|
|
||||||
session: Session,
|
|
||||||
loginSettings: LoginSettings | undefined,
|
|
||||||
authMethods: AuthenticationMethodType[],
|
|
||||||
organization?: string,
|
|
||||||
authRequestId?: string,
|
|
||||||
) {
|
|
||||||
const availableMultiFactors = authMethods?.filter(
|
|
||||||
(m: AuthenticationMethodType) =>
|
|
||||||
m !== AuthenticationMethodType.PASSWORD &&
|
|
||||||
m !== AuthenticationMethodType.PASSKEY,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (availableMultiFactors?.length == 1) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
loginName: session.factors?.user?.loginName as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authRequestId) {
|
|
||||||
params.append("authRequestId", authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization || session.factors?.user?.organizationId) {
|
|
||||||
params.append(
|
|
||||||
"organization",
|
|
||||||
organization ?? (session.factors?.user?.organizationId as string),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const factor = availableMultiFactors[0];
|
|
||||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
|
||||||
if (factor === AuthenticationMethodType.TOTP) {
|
|
||||||
return { redirect: `/otp/time-based?` + params };
|
|
||||||
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
|
||||||
return { redirect: `/otp/sms?` + params };
|
|
||||||
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
|
||||||
return { redirect: `/otp/email?` + params };
|
|
||||||
} else if (factor === AuthenticationMethodType.U2F) {
|
|
||||||
return { redirect: `/u2f?` + params };
|
|
||||||
}
|
|
||||||
} else if (availableMultiFactors?.length >= 1) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
loginName: session.factors?.user?.loginName as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authRequestId) {
|
|
||||||
params.append("authRequestId", authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization || session.factors?.user?.organizationId) {
|
|
||||||
params.append(
|
|
||||||
"organization",
|
|
||||||
organization ?? (session.factors?.user?.organizationId as string),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { redirect: `/mfa?` + params };
|
|
||||||
} else if (
|
|
||||||
(loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) &&
|
|
||||||
!availableMultiFactors.length
|
|
||||||
) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
loginName: session.factors?.user?.loginName as string,
|
|
||||||
force: "true", // this defines if the mfa is forced in the settings
|
|
||||||
checkAfter: "true", // this defines if the check is directly made after the setup
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authRequestId) {
|
|
||||||
params.append("authRequestId", authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement passkey setup
|
|
||||||
|
|
||||||
// else if (
|
|
||||||
// submitted.factors &&
|
|
||||||
// !submitted.factors.webAuthN && // if session was not verified with a passkey
|
|
||||||
// promptPasswordless && // if explicitly prompted due policy
|
|
||||||
// !isAlternative // escaped if password was used as an alternative method
|
|
||||||
// ) {
|
|
||||||
// const params = new URLSearchParams({
|
|
||||||
// loginName: submitted.factors.user.loginName,
|
|
||||||
// prompt: "true",
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (authRequestId) {
|
|
||||||
// params.append("authRequestId", authRequestId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (organization) {
|
|
||||||
// params.append("organization", organization);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return router.push(`/passkey/set?` + params);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
@@ -14,13 +14,16 @@ import { create } from "@zitadel/client";
|
|||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
|
import { headers } from "next/headers";
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieByLoginName } from "../cookies";
|
||||||
|
import { checkMFAFactors } from "../verify-helper";
|
||||||
import { createSessionAndUpdateCookie } from "./cookie";
|
import { createSessionAndUpdateCookie } from "./cookie";
|
||||||
import { checkMFAFactors } from "./password";
|
|
||||||
|
|
||||||
type VerifyUserByEmailCommand = {
|
type VerifyUserByEmailCommand = {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
loginName?: string; // to determine already existing session
|
||||||
|
organization?: string;
|
||||||
code: string;
|
code: string;
|
||||||
isInvite: boolean;
|
isInvite: boolean;
|
||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
@@ -39,82 +42,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
return { error: "Could not verify user" };
|
return { error: "Could not verify user" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResponse = await getUserByID(command.userId);
|
|
||||||
|
|
||||||
if (!userResponse || !userResponse.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
|
||||||
user: {
|
|
||||||
search: {
|
|
||||||
case: "loginName",
|
|
||||||
value: userResponse.user.preferredLoginName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const session = await createSessionAndUpdateCookie(
|
|
||||||
checks,
|
|
||||||
undefined,
|
|
||||||
command.authRequestId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const authMethodResponse = await listAuthenticationMethodTypes(
|
|
||||||
command.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
|
||||||
return { error: "Could not load possible authenticators" };
|
|
||||||
}
|
|
||||||
// if no authmethods are found on the user, redirect to set one up
|
|
||||||
if (
|
|
||||||
authMethodResponse &&
|
|
||||||
authMethodResponse.authMethodTypes &&
|
|
||||||
authMethodResponse.authMethodTypes.length == 0
|
|
||||||
) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
sessionId: session.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (session.factors?.user?.loginName) {
|
|
||||||
params.set("loginName", session.factors?.user?.loginName);
|
|
||||||
}
|
|
||||||
return { redirect: `/authenticator/set?${params}` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type resendVerifyEmailCommand = {
|
|
||||||
userId: string;
|
|
||||||
isInvite: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function resendVerification(command: resendVerifyEmailCommand) {
|
|
||||||
return command.isInvite
|
|
||||||
? resendInviteCode(command.userId)
|
|
||||||
: resendEmailCode(command.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SendVerificationRedirectWithoutCheckCommand = {
|
|
||||||
organization?: string;
|
|
||||||
authRequestId?: string;
|
|
||||||
} & (
|
|
||||||
| { userId: string; loginName?: never }
|
|
||||||
| { userId?: never; loginName: string }
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function sendVerificationRedirectWithoutCheck(
|
|
||||||
command: SendVerificationRedirectWithoutCheckCommand,
|
|
||||||
) {
|
|
||||||
if (!("loginName" in command || "userId" in command)) {
|
|
||||||
return { error: "No userId, nor loginname provided" };
|
|
||||||
}
|
|
||||||
|
|
||||||
let session: Session | undefined;
|
let session: Session | undefined;
|
||||||
let user: User | undefined;
|
let user: User | undefined;
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings(command.organization);
|
|
||||||
|
|
||||||
if ("loginName" in command) {
|
if ("loginName" in command) {
|
||||||
const sessionCookie = await getSessionCookieByLoginName({
|
const sessionCookie = await getSessionCookieByLoginName({
|
||||||
loginName: command.loginName,
|
loginName: command.loginName,
|
||||||
@@ -147,10 +77,10 @@ export async function sendVerificationRedirectWithoutCheck(
|
|||||||
}
|
}
|
||||||
|
|
||||||
user = userResponse.user;
|
user = userResponse.user;
|
||||||
} else if ("userId" in command) {
|
} else {
|
||||||
const userResponse = await getUserByID(command.userId);
|
const userResponse = await getUserByID(command.userId);
|
||||||
|
|
||||||
if (!userResponse?.user) {
|
if (!userResponse || !userResponse.user) {
|
||||||
return { error: "Could not load user" };
|
return { error: "Could not load user" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +100,6 @@ export async function sendVerificationRedirectWithoutCheck(
|
|||||||
undefined,
|
undefined,
|
||||||
command.authRequestId,
|
command.authRequestId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// this is a fake error message to hide that the user does not even exist
|
|
||||||
return { error: "Could not verify password" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
if (!session?.factors?.user?.id) {
|
||||||
@@ -187,6 +114,8 @@ export async function sendVerificationRedirectWithoutCheck(
|
|||||||
return { error: "Could not load user" };
|
return { error: "Could not load user" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(user.details?.resourceOwner);
|
||||||
|
|
||||||
const authMethodResponse = await listAuthenticationMethodTypes(user.userId);
|
const authMethodResponse = await listAuthenticationMethodTypes(user.userId);
|
||||||
|
|
||||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||||
@@ -243,3 +172,163 @@ export async function sendVerificationRedirectWithoutCheck(
|
|||||||
|
|
||||||
return { redirect: url };
|
return { redirect: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resendVerifyEmailCommand = {
|
||||||
|
userId: string;
|
||||||
|
isInvite: boolean;
|
||||||
|
authRequestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function resendVerification(command: resendVerifyEmailCommand) {
|
||||||
|
const host = (await headers()).get("host");
|
||||||
|
|
||||||
|
return command.isInvite
|
||||||
|
? resendInviteCode(command.userId)
|
||||||
|
: resendEmailCode(command.userId, host, command.authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SendVerificationRedirectWithoutCheckCommand = {
|
||||||
|
organization?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
} & (
|
||||||
|
| { userId: string; loginName?: never }
|
||||||
|
| { userId?: never; loginName: string }
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function sendVerificationRedirectWithoutCheck(
|
||||||
|
command: SendVerificationRedirectWithoutCheckCommand,
|
||||||
|
) {
|
||||||
|
if (!("loginName" in command || "userId" in command)) {
|
||||||
|
return { error: "No userId, nor loginname provided" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let session: Session | undefined;
|
||||||
|
let user: User | undefined;
|
||||||
|
|
||||||
|
if ("loginName" in command) {
|
||||||
|
const sessionCookie = await getSessionCookieByLoginName({
|
||||||
|
loginName: command.loginName,
|
||||||
|
organization: command.organization,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn("Ignored error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
return { error: "Could not load session cookie" };
|
||||||
|
}
|
||||||
|
|
||||||
|
session = await getSession({
|
||||||
|
sessionId: sessionCookie.id,
|
||||||
|
sessionToken: sessionCookie.token,
|
||||||
|
}).then((response) => {
|
||||||
|
if (response?.session) {
|
||||||
|
return response.session;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = await getUserByID(session?.factors?.user?.id);
|
||||||
|
|
||||||
|
if (!userResponse?.user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userResponse.user;
|
||||||
|
} else if ("userId" in command) {
|
||||||
|
const userResponse = await getUserByID(command.userId);
|
||||||
|
|
||||||
|
if (!userResponse?.user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userResponse.user;
|
||||||
|
|
||||||
|
const checks = create(ChecksSchema, {
|
||||||
|
user: {
|
||||||
|
search: {
|
||||||
|
case: "loginName",
|
||||||
|
value: userResponse.user.preferredLoginName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
session = await createSessionAndUpdateCookie(
|
||||||
|
checks,
|
||||||
|
undefined,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return { error: "Could not load user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const authMethodResponse = await listAuthenticationMethodTypes(user.userId);
|
||||||
|
|
||||||
|
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||||
|
return { error: "Could not load possible authenticators" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no authmethods are found on the user, redirect to set one up
|
||||||
|
if (
|
||||||
|
authMethodResponse &&
|
||||||
|
authMethodResponse.authMethodTypes &&
|
||||||
|
authMethodResponse.authMethodTypes.length == 0
|
||||||
|
) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
sessionId: session.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session.factors?.user?.loginName) {
|
||||||
|
params.set("loginName", session.factors?.user?.loginName);
|
||||||
|
}
|
||||||
|
return { redirect: `/authenticator/set?${params}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(user.details?.resourceOwner);
|
||||||
|
|
||||||
|
// redirect to mfa factor if user has one, or redirect to set one up
|
||||||
|
checkMFAFactors(
|
||||||
|
session,
|
||||||
|
loginSettings,
|
||||||
|
authMethodResponse.authMethodTypes,
|
||||||
|
command.organization,
|
||||||
|
command.authRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// login user if no additional steps are required
|
||||||
|
if (command.authRequestId && session.id) {
|
||||||
|
const nextUrl = await getNextUrl(
|
||||||
|
{
|
||||||
|
sessionId: session.id,
|
||||||
|
authRequestId: command.authRequestId,
|
||||||
|
organization:
|
||||||
|
command.organization ?? session.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { redirect: nextUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: session.factors.user.loginName,
|
||||||
|
organization: session.factors?.user?.organizationId,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { redirect: url };
|
||||||
|
}
|
||||||
|
110
apps/login/src/lib/verify-helper.ts
Normal file
110
apps/login/src/lib/verify-helper.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
|
|
||||||
|
export function checkMFAFactors(
|
||||||
|
session: Session,
|
||||||
|
loginSettings: LoginSettings | undefined,
|
||||||
|
authMethods: AuthenticationMethodType[],
|
||||||
|
organization?: string,
|
||||||
|
authRequestId?: string,
|
||||||
|
) {
|
||||||
|
const availableMultiFactors = authMethods?.filter(
|
||||||
|
(m: AuthenticationMethodType) =>
|
||||||
|
m !== AuthenticationMethodType.PASSWORD &&
|
||||||
|
m !== AuthenticationMethodType.PASSKEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (availableMultiFactors?.length == 1) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (session.factors?.user?.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = availableMultiFactors[0];
|
||||||
|
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||||
|
if (factor === AuthenticationMethodType.TOTP) {
|
||||||
|
return { redirect: `/otp/time-based?` + params };
|
||||||
|
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
||||||
|
return { redirect: `/otp/sms?` + params };
|
||||||
|
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
||||||
|
return { redirect: `/otp/email?` + params };
|
||||||
|
} else if (factor === AuthenticationMethodType.U2F) {
|
||||||
|
return { redirect: `/u2f?` + params };
|
||||||
|
}
|
||||||
|
} else if (availableMultiFactors?.length >= 1) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (session.factors?.user?.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect: `/mfa?` + params };
|
||||||
|
} else if (
|
||||||
|
(loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) &&
|
||||||
|
!availableMultiFactors.length
|
||||||
|
) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
force: "true", // this defines if the mfa is forced in the settings
|
||||||
|
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement passkey setup
|
||||||
|
|
||||||
|
// else if (
|
||||||
|
// submitted.factors &&
|
||||||
|
// !submitted.factors.webAuthN && // if session was not verified with a passkey
|
||||||
|
// promptPasswordless && // if explicitly prompted due policy
|
||||||
|
// !isAlternative // escaped if password was used as an alternative method
|
||||||
|
// ) {
|
||||||
|
// const params = new URLSearchParams({
|
||||||
|
// loginName: submitted.factors.user.loginName,
|
||||||
|
// prompt: "true",
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (authRequestId) {
|
||||||
|
// params.append("authRequestId", authRequestId);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (organization) {
|
||||||
|
// params.append("organization", organization);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return router.push(`/passkey/set?` + params);
|
||||||
|
// }
|
||||||
|
}
|
@@ -12,6 +12,8 @@ import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_p
|
|||||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
|
ResendEmailCodeRequest,
|
||||||
|
ResendEmailCodeRequestSchema,
|
||||||
RetrieveIdentityProviderIntentRequest,
|
RetrieveIdentityProviderIntentRequest,
|
||||||
SetPasswordRequest,
|
SetPasswordRequest,
|
||||||
SetPasswordRequestSchema,
|
SetPasswordRequestSchema,
|
||||||
@@ -23,6 +25,7 @@ import { create, Duration } from "@zitadel/client";
|
|||||||
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
||||||
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
|
import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb";
|
||||||
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@@ -448,13 +451,28 @@ export async function verifyEmail(userId: string, verificationCode: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resendEmailCode(userId: string) {
|
export async function resendEmailCode(
|
||||||
return userService.resendEmailCode(
|
userId: string,
|
||||||
{
|
host: string | null,
|
||||||
userId,
|
authRequestId?: string,
|
||||||
},
|
) {
|
||||||
{},
|
let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, {
|
||||||
);
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
const medium = create(SendEmailVerificationCodeSchema, {
|
||||||
|
urlTemplate:
|
||||||
|
`${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||||
|
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
|
||||||
|
});
|
||||||
|
|
||||||
|
request = { ...request, verification: { case: "sendCode", value: medium } };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(request);
|
||||||
|
|
||||||
|
return userService.resendEmailCode(request, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function retrieveIDPIntent(id: string, token: string) {
|
export function retrieveIDPIntent(id: string, token: string) {
|
||||||
|
Reference in New Issue
Block a user