Merge pull request #465 from zitadel/qa

Promote qa to prod
This commit is contained in:
Max Peintner
2025-05-28 13:19:10 +02:00
committed by GitHub
22 changed files with 504 additions and 633 deletions

View File

@@ -174,13 +174,15 @@
}, },
"verify": { "verify": {
"userIdMissing": "Keine Benutzer-ID angegeben!", "userIdMissing": "Keine Benutzer-ID angegeben!",
"success": "Erfolgreich verifiziert", "successTitle": "Benutzer verifiziert",
"successDescription": "Der Benutzer wurde erfolgreich verifiziert.",
"setupAuthenticator": "Authentifikator einrichten", "setupAuthenticator": "Authentifikator einrichten",
"verify": { "verify": {
"title": "Benutzer verifizieren", "title": "Benutzer verifizieren",
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.", "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
"noCodeReceived": "Keinen Code erhalten?", "noCodeReceived": "Keinen Code erhalten?",
"resendCode": "Code erneut senden", "resendCode": "Code erneut senden",
"codeSent": "Ein Code wurde gerade an Ihre E-Mail-Adresse gesendet.",
"submit": "Weiter" "submit": "Weiter"
} }
}, },

View File

@@ -174,13 +174,15 @@
}, },
"verify": { "verify": {
"userIdMissing": "No userId provided!", "userIdMissing": "No userId provided!",
"success": "The user has been verified successfully.", "successTitle": "User verified",
"successDescription": "The user has been verified successfully.",
"setupAuthenticator": "Setup authenticator", "setupAuthenticator": "Setup authenticator",
"verify": { "verify": {
"title": "Verify user", "title": "Verify user",
"description": "Enter the Code provided in the verification email.", "description": "Enter the Code provided in the verification email.",
"noCodeReceived": "Didn't receive a code?", "noCodeReceived": "Didn't receive a code?",
"resendCode": "Resend code", "resendCode": "Resend code",
"codeSent": "A code has just been sent to your email address.",
"submit": "Continue" "submit": "Continue"
} }
}, },

View File

@@ -174,13 +174,15 @@
}, },
"verify": { "verify": {
"userIdMissing": "¡No se proporcionó userId!", "userIdMissing": "¡No se proporcionó userId!",
"success": "¡Verificación exitosa!", "successTitle": "Usuario verificado",
"successDescription": "El usuario ha sido verificado con éxito.",
"setupAuthenticator": "Configurar autenticador", "setupAuthenticator": "Configurar autenticador",
"verify": { "verify": {
"title": "Verificar usuario", "title": "Verificar usuario",
"description": "Introduce el código proporcionado en el correo electrónico de verificación.", "description": "Introduce el código proporcionado en el correo electrónico de verificación.",
"noCodeReceived": "¿No recibiste un código?", "noCodeReceived": "¿No recibiste un código?",
"resendCode": "Reenviar código", "resendCode": "Reenviar código",
"codeSent": "Se ha enviado un código a tu dirección de correo electrónico.",
"submit": "Continuar" "submit": "Continuar"
} }
}, },

View File

@@ -174,13 +174,15 @@
}, },
"verify": { "verify": {
"userIdMissing": "Nessun userId fornito!", "userIdMissing": "Nessun userId fornito!",
"success": "Verifica effettuata con successo!", "successTitle": "Utente verificato",
"successDescription": "L'utente è stato verificato con successo.",
"setupAuthenticator": "Configura autenticatore", "setupAuthenticator": "Configura autenticatore",
"verify": { "verify": {
"title": "Verifica utente", "title": "Verifica utente",
"description": "Inserisci il codice fornito nell'email di verifica.", "description": "Inserisci il codice fornito nell'email di verifica.",
"noCodeReceived": "Non hai ricevuto un codice?", "noCodeReceived": "Non hai ricevuto un codice?",
"resendCode": "Invia di nuovo il codice", "resendCode": "Invia di nuovo il codice",
"codeSent": "Un codice è stato appena inviato al tuo indirizzo email.",
"submit": "Continua" "submit": "Continua"
} }
}, },

View File

@@ -174,13 +174,15 @@
}, },
"verify": { "verify": {
"userIdMissing": "Nie podano identyfikatora użytkownika!", "userIdMissing": "Nie podano identyfikatora użytkownika!",
"success": "Użytkownik został pomyślnie zweryfikowany.", "successTitle": "Weryfikacja zakończona",
"successDescription": "Użytkownik został pomyślnie zweryfikowany.",
"setupAuthenticator": "Skonfiguruj uwierzytelnianie", "setupAuthenticator": "Skonfiguruj uwierzytelnianie",
"verify": { "verify": {
"title": "Zweryfikuj użytkownika", "title": "Zweryfikuj użytkownika",
"description": "Wprowadź kod z wiadomości weryfikacyjnej.", "description": "Wprowadź kod z wiadomości weryfikacyjnej.",
"noCodeReceived": "Nie otrzymałeś kodu?", "noCodeReceived": "Nie otrzymałeś kodu?",
"resendCode": "Wyślij kod ponownie", "resendCode": "Wyślij kod ponownie",
"codeSent": "Kod został właśnie wysłany na twój adres e-mail.",
"submit": "Kontynuuj" "submit": "Kontynuuj"
} }
}, },

View File

@@ -174,13 +174,15 @@
}, },
"verify": { "verify": {
"userIdMissing": "Не указан userId!", "userIdMissing": "Не указан userId!",
"success": "Пользователь успешно подтверждён.", "successTitle": "Пользователь подтверждён",
"successDescription": "Пользователь успешно подтверждён.",
"setupAuthenticator": "Настроить аутентификатор", "setupAuthenticator": "Настроить аутентификатор",
"verify": { "verify": {
"title": "Подтверждение пользователя", "title": "Подтверждение пользователя",
"description": "Введите код из письма подтверждения.", "description": "Введите код из письма подтверждения.",
"noCodeReceived": "Не получили код?", "noCodeReceived": "Не получили код?",
"resendCode": "Отправить код повторно", "resendCode": "Отправить код повторно",
"codeSent": "Код отправлен на ваш email.",
"submit": "Продолжить" "submit": "Продолжить"
} }
}, },

View File

@@ -174,13 +174,15 @@
}, },
"verify": { "verify": {
"userIdMissing": "未提供用户 ID", "userIdMissing": "未提供用户 ID",
"success": "用户验证成功。", "successTitle": "用户验证",
"successDescription": "用户已成功验证。",
"setupAuthenticator": "设置认证器", "setupAuthenticator": "设置认证器",
"verify": { "verify": {
"title": "验证用户", "title": "验证用户",
"description": "输入验证邮件中的验证码。", "description": "输入验证邮件中的验证码。",
"noCodeReceived": "没有收到验证码?", "noCodeReceived": "没有收到验证码?",
"resendCode": "重发验证码", "resendCode": "重发验证码",
"codeSent": "刚刚发送了一封包含验证码的电子邮件。",
"submit": "继续" "submit": "继续"
} }
}, },

View File

@@ -7,6 +7,7 @@ import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { checkUserVerification } from "@/lib/verify-helper";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,
getBrandingSettings, getBrandingSettings,
@@ -18,6 +19,7 @@ import {
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
@@ -92,20 +94,50 @@ export default async function Page(props: {
}); });
} }
if (!sessionWithData) { if (
!sessionWithData ||
!sessionWithData.factors ||
!sessionWithData.factors.user
) {
return <Alert>{tError("unknownContext")}</Alert>; return <Alert>{tError("unknownContext")}</Alert>;
} }
const branding = await getBrandingSettings({ const branding = await getBrandingSettings({
serviceUrl, serviceUrl,
organization: sessionWithData.factors?.user?.organizationId, organization: sessionWithData.factors.user?.organizationId,
}); });
const loginSettings = await getLoginSettings({ const loginSettings = await getLoginSettings({
serviceUrl, serviceUrl,
organization: sessionWithData.factors?.user?.organizationId, organization: sessionWithData.factors.user?.organizationId,
}); });
// check if user was verified recently
const isUserVerified = await checkUserVerification(
sessionWithData.factors.user?.id,
);
if (!isUserVerified) {
const params = new URLSearchParams({
loginName: sessionWithData.factors.user.loginName as string,
invite: "true",
send: "true", // set this to true to request a new code immediately
});
if (requestId) {
params.append("requestId", requestId);
}
if (organization || sessionWithData.factors.user.organizationId) {
params.append(
"organization",
organization ?? (sessionWithData.factors.user.organizationId as string),
);
}
redirect(`/verify?` + params);
}
const identityProviders = await getActiveIdentityProviders({ const identityProviders = await getActiveIdentityProviders({
serviceUrl, serviceUrl,
orgId: sessionWithData.factors?.user?.organizationId, orgId: sessionWithData.factors?.user?.organizationId,
@@ -152,13 +184,12 @@ export default async function Page(props: {
></ChooseAuthenticatorToSetup> ></ChooseAuthenticatorToSetup>
)} )}
{loginSettings?.allowExternalIdp && identityProviders && ( {loginSettings?.allowExternalIdp && !!identityProviders.length && (
<> <>
{identityProviders.length && (
<div className="py-3 flex flex-col"> <div className="py-3 flex flex-col">
<p className="ztdl-p text-center">{t("linkWithIDP")}</p> <p className="ztdl-p text-center">{t("linkWithIDP")}</p>
</div> </div>
)}
<SignInWithIdp <SignInWithIdp
identityProviders={identityProviders} identityProviders={identityProviders}
requestId={requestId} requestId={requestId}

View File

@@ -1,73 +0,0 @@
import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme";
import { InviteForm } from "@/components/invite-form";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import {
getBrandingSettings,
getDefaultOrg,
getLoginSettings,
getPasswordComplexitySettings,
} from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "invite" });
let { firstname, lastname, email, organization } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
if (!organization) {
const org = await getDefaultOrg({ serviceUrl });
if (!org) {
throw new Error("No default organization found");
}
organization = org.id;
}
const loginSettings = await getLoginSettings({
serviceUrl,
organization,
});
const passwordComplexitySettings = await getPasswordComplexitySettings({
serviceUrl,
organization,
});
const branding = await getBrandingSettings({
serviceUrl,
organization,
});
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("title")}</h1>
<p className="ztdl-p">{t("description")}</p>
{!loginSettings?.allowRegister ? (
<Alert type={AlertType.ALERT}>{t("notAllowed")}</Alert>
) : (
<Alert type={AlertType.INFO}>{t("info")}</Alert>
)}
{passwordComplexitySettings && loginSettings?.allowRegister && (
<InviteForm
organization={organization}
firstname={firstname}
lastname={lastname}
email={email}
></InviteForm>
)}
</div>
</DynamicTheme>
);
}

View File

@@ -1,81 +0,0 @@
import { Alert, AlertType } from "@/components/alert";
import { Button, ButtonVariants } from "@/components/button";
import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
import Link from "next/link";
export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "invite" });
let { userId, organization } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
if (!organization) {
const org = await getDefaultOrg({ serviceUrl });
if (!org) {
throw new Error("No default organization found");
}
organization = org.id;
}
const branding = await getBrandingSettings({
serviceUrl,
organization,
});
let user: User | undefined;
let human: HumanUser | undefined;
if (userId) {
const userResponse = await getUserByID({
serviceUrl,
userId,
});
if (userResponse) {
user = userResponse.user;
if (user?.type.case === "human") {
human = user.type.value as HumanUser;
}
}
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("success.title")}</h1>
<p className="ztdl-p">{t("success.description")}</p>
{user && (
<UserAvatar
loginName={user.preferredLoginName}
displayName={human?.profile?.displayName}
showDropdown={false}
/>
)}
{human?.email?.isVerified ? (
<Alert type={AlertType.INFO}>{t("success.verified")}</Alert>
) : (
<Alert type={AlertType.INFO}>{t("success.notVerifiedYet")}</Alert>
)}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<span></span>
<Link href="/invite">
<Button type="submit" variant={ButtonVariants.Primary}>
{t("success.submit")}
</Button>
</Link>
</div>
</div>
</DynamicTheme>
);
}

View File

@@ -1,18 +1,12 @@
import { Alert } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { VerifyForm } from "@/components/verify-form"; import { VerifyForm } from "@/components/verify-form";
import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify";
import { sendEmailCode } from "@/lib/server/verify";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
getBrandingSettings,
getUserByID,
listAuthenticationMethodTypes,
} from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
@@ -22,16 +16,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
const t = await getTranslations({ locale, namespace: "verify" }); const t = await getTranslations({ locale, namespace: "verify" });
const tError = await getTranslations({ locale, namespace: "error" }); const tError = await getTranslations({ locale, namespace: "error" });
const { userId, loginName, code, organization, requestId, invite } = const { userId, loginName, code, organization, requestId, invite, send } =
searchParams; searchParams;
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = _headers.get("host");
if (!host || typeof host !== "string") {
throw new Error("No host found");
}
const branding = await getBrandingSettings({ const branding = await getBrandingSettings({
serviceUrl, serviceUrl,
@@ -43,10 +32,40 @@ export default async function Page(props: { searchParams: Promise<any> }) {
let human: HumanUser | undefined; let human: HumanUser | undefined;
let id: string | undefined; let id: string | undefined;
const doSend = invite !== "true"; const doSend = send === "true";
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
async function sendEmail(userId: string) {
const host = _headers.get("host");
if (!host || typeof host !== "string") {
throw new Error("No host found");
}
if (invite === "true") {
await sendInviteEmailCode({
userId,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => {
console.error("Could not send invitation email", error);
throw Error("Failed to send invitation email");
});
} else {
await sendEmailCode({
userId,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => {
console.error("Could not send verification email", error);
throw Error("Failed to send verification email");
});
}
}
if ("loginName" in searchParams) { if ("loginName" in searchParams) {
sessionFactors = await loadMostRecentSession({ sessionFactors = await loadMostRecentSession({
serviceUrl, serviceUrl,
@@ -57,29 +76,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
}); });
if (doSend && sessionFactors?.factors?.user?.id) { if (doSend && sessionFactors?.factors?.user?.id) {
await sendEmailCode({ await sendEmail(sessionFactors.factors.user.id);
serviceUrl,
userId: sessionFactors?.factors?.user?.id,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => {
console.error("Could not resend verification email", error);
throw Error("Failed to send verification email");
});
} }
} else if ("userId" in searchParams && userId) { } else if ("userId" in searchParams && userId) {
if (doSend) { if (doSend) {
await sendEmailCode({ await sendEmail(userId);
serviceUrl,
userId,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => {
console.error("Could not resend verification email", error);
throw Error("Failed to send verification email");
});
} }
const userResponse = await getUserByID({ const userResponse = await getUserByID({
@@ -96,12 +97,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
id = userId ?? sessionFactors?.factors?.user?.id; id = userId ?? sessionFactors?.factors?.user?.id;
let authMethods: AuthenticationMethodType[] | null = null; if (!id) {
if (human?.email?.isVerified) { throw Error("Failed to get user id");
const authMethodsResponse = await listAuthenticationMethodTypes(userId);
if (authMethodsResponse.authMethodTypes) {
authMethods = authMethodsResponse.authMethodTypes;
}
} }
const params = new URLSearchParams({ const params = new URLSearchParams({
@@ -138,6 +135,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
</> </>
)} )}
{id && send && (
<div className="py-4 w-full">
<Alert type={AlertType.INFO}>{t("verify.codeSent")}</Alert>
</div>
)}
{sessionFactors ? ( {sessionFactors ? (
<UserAvatar <UserAvatar
loginName={loginName ?? sessionFactors.factors?.user?.loginName} loginName={loginName ?? sessionFactors.factors?.user?.loginName}
@@ -155,18 +158,6 @@ export default async function Page(props: { searchParams: Promise<any> }) {
) )
)} )}
{id &&
(human?.email?.isVerified ? (
// show page for already verified users
<VerifyRedirectButton
userId={id}
loginName={loginName}
organization={organization}
requestId={requestId}
authMethods={authMethods}
/>
) : (
// check if auth methods are set
<VerifyForm <VerifyForm
loginName={loginName} loginName={loginName}
organization={organization} organization={organization}
@@ -175,7 +166,6 @@ export default async function Page(props: { searchParams: Promise<any> }) {
isInvite={invite === "true"} isInvite={invite === "true"}
requestId={requestId} requestId={requestId}
/> />
))}
</div> </div>
</DynamicTheme> </DynamicTheme>
); );

View File

@@ -0,0 +1,109 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import {
getBrandingSettings,
getLoginSettings,
getSession,
getUserByID,
} from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
async function loadSessionById(
serviceUrl: string,
sessionId: string,
organization?: string,
) {
const recent = await getSessionCookieById({ sessionId, organization });
return getSession({
serviceUrl,
sessionId: recent.id,
sessionToken: recent.token,
}).then((response) => {
if (response?.session) {
return response.session;
}
});
}
export default async function Page(props: { searchParams: Promise<any> }) {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "verify" });
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { loginName, requestId, organization, userId } = searchParams;
const branding = await getBrandingSettings({
serviceUrl,
organization,
});
const sessionFactors = await loadMostRecentSession({
serviceUrl,
sessionParams: { loginName, organization },
}).catch((error) => {
console.warn("Error loading session:", error);
});
let loginSettings;
if (!requestId) {
loginSettings = await getLoginSettings({
serviceUrl,
organization,
});
}
const id = userId ?? sessionFactors?.factors?.user?.id;
if (!id) {
throw Error("Failed to get user id");
}
const userResponse = await getUserByID({
serviceUrl,
userId: id,
});
let user: User | undefined;
let human: HumanUser | undefined;
if (userResponse) {
user = userResponse.user;
if (user?.type.case === "human") {
human = user.type.value as HumanUser;
}
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
<h1>{t("successTitle")}</h1>
<p className="ztdl-p mb-6 block">{t("successDescription")}</p>
{sessionFactors ? (
<UserAvatar
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
displayName={sessionFactors.factors?.user?.displayName}
showDropdown
searchParams={searchParams}
></UserAvatar>
) : (
user && (
<UserAvatar
loginName={user.preferredLoginName}
displayName={human?.profile?.displayName}
showDropdown={false}
/>
)
)}
</div>
</DynamicTheme>
);
}

View File

@@ -83,6 +83,16 @@ export function RegisterPasskey({
return; return;
} }
if ("error" in resp && resp.error) {
setError(resp.error);
return;
}
if (!("passkeyId" in resp)) {
setError("An error on registering passkey");
return;
}
const passkeyId = resp.passkeyId; const passkeyId = resp.passkeyId;
const options: CredentialCreationOptions = const options: CredentialCreationOptions =
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? (resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
@@ -92,6 +102,7 @@ export function RegisterPasskey({
setError("An error on registering passkey"); setError("An error on registering passkey");
return; return;
} }
options.publicKey.challenge = coerceToArrayBuffer( options.publicKey.challenge = coerceToArrayBuffer(
options.publicKey.challenge, options.publicKey.challenge,
"challenge", "challenge",

View File

@@ -74,7 +74,7 @@ export function SignInWithIdp({
return ( return (
<div className="flex flex-col w-full space-y-2 text-sm"> <div className="flex flex-col w-full space-y-2 text-sm">
{identityProviders?.map(renderIDPButton)} {!!identityProviders.length && identityProviders?.map(renderIDPButton)}
{state?.error && ( {state?.error && (
<div className="py-4"> <div className="py-4">
<Alert>{state?.error}</Alert> <Alert>{state?.error}</Alert>

View File

@@ -63,6 +63,11 @@ export function VerifyForm({
setLoading(false); setLoading(false);
}); });
if (response && "error" in response && response?.error) {
setError(response.error);
return;
}
return response; return response;
} }

View File

@@ -1,90 +0,0 @@
"use client";
import {
sendVerificationRedirectWithoutCheck,
SendVerificationRedirectWithoutCheckCommand,
} from "@/lib/server/verify";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { Alert, AlertType } from "./alert";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { Spinner } from "./spinner";
export function VerifyRedirectButton({
userId,
loginName,
requestId,
authMethods,
organization,
}: {
userId?: string;
loginName?: string;
requestId: string;
authMethods: AuthenticationMethodType[] | null;
organization?: string;
}) {
const t = useTranslations("verify");
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
async function submitAndContinue(): Promise<boolean | void> {
setLoading(true);
let command = {
organization,
requestId,
} as SendVerificationRedirectWithoutCheckCommand;
if (userId) {
command = {
...command,
userId,
} as SendVerificationRedirectWithoutCheckCommand;
} else if (loginName) {
command = {
...command,
loginName,
} as SendVerificationRedirectWithoutCheckCommand;
}
await sendVerificationRedirectWithoutCheck(command)
.catch(() => {
setError("Could not verify");
return;
})
.finally(() => {
setLoading(false);
});
}
return (
<>
<Alert type={AlertType.INFO}>{t("success")}</Alert>
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<BackButton />
<span className="flex-grow"></span>
{authMethods?.length === 0 && (
<Button
onClick={() => submitAndContinue()}
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
{t("setupAuthenticator")}
</Button>
)}
</div>
</>
);
}

View File

@@ -9,7 +9,6 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkInvite } from "../verify-helper";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,
getIDPByID, getIDPByID,
@@ -254,37 +253,27 @@ export async function sendLoginname(command: SendLoginnameCommand) {
userId: session.factors?.user?.id, userId: session.factors?.user?.id,
}); });
// this can be expected to be an invite as users created in console have a password set. // always resend invite if user has no auth method set
if (!methods.authMethodTypes || !methods.authMethodTypes.length) { if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
// redirect to /verify invite if no auth method is set and email is not verified const params = new URLSearchParams({
const inviteCheck = checkInvite( loginName: session.factors?.user?.loginName as string,
session, send: "true", // set this to true to request a new code immediately
humanUser, invite: "true",
session.factors.user.organizationId,
command.requestId,
);
if (inviteCheck?.redirect) {
return inviteCheck;
}
const paramsAuthenticatorSetup = new URLSearchParams({
loginName: session.factors?.user?.loginName,
userId: session.factors?.user?.id, // verify needs user id
}); });
if (command.requestId) {
params.append("requestId", command.requestId);
}
if (command.organization || session.factors?.user?.organizationId) { if (command.organization || session.factors?.user?.organizationId) {
paramsAuthenticatorSetup.append( params.append(
"organization", "organization",
command.organization ?? session.factors?.user?.organizationId, command.organization ??
(session.factors?.user?.organizationId as string),
); );
} }
if (command.requestId) { return { redirect: `/verify?` + params };
paramsAuthenticatorSetup.append("requestId", command.requestId);
}
return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup };
} }
if (methods.authMethodTypes.length == 1) { if (methods.authMethodTypes.length == 1) {

View File

@@ -5,10 +5,12 @@ import {
getLoginSettings, getLoginSettings,
getSession, getSession,
getUserByID, getUserByID,
listAuthenticationMethodTypes,
registerPasskey, registerPasskey,
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { create, Duration } from "@zitadel/client"; import { create, Duration, Timestamp, timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { import {
RegisterPasskeyResponse, RegisterPasskeyResponse,
@@ -23,7 +25,10 @@ import {
getSessionCookieByLoginName, getSessionCookieByLoginName,
} from "../cookies"; } from "../cookies";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkEmailVerification } from "../verify-helper"; import {
checkEmailVerification,
checkUserVerification,
} from "../verify-helper";
import { setSessionAndUpdateCookie } from "./cookie"; import { setSessionAndUpdateCookie } from "./cookie";
type VerifyPasskeyCommand = { type VerifyPasskeyCommand = {
@@ -37,9 +42,25 @@ type RegisterPasskeyCommand = {
sessionId: string; sessionId: string;
}; };
function isSessionValid(session: Partial<Session>): {
valid: boolean;
verifiedAt?: Timestamp;
} {
const validPassword = session?.factors?.password?.verifiedAt;
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
const stillValid = session.expirationDate
? timestampDate(session.expirationDate) > new Date()
: true;
const verifiedAt = validPassword || validPasskey;
const valid = !!((validPassword || validPasskey) && stillValid);
return { valid, verifiedAt };
}
export async function registerPasskeyLink( export async function registerPasskeyLink(
command: RegisterPasskeyCommand, command: RegisterPasskeyCommand,
): Promise<RegisterPasskeyResponse> { ): Promise<RegisterPasskeyResponse | { error: string }> {
const { sessionId } = command; const { sessionId } = command;
const _headers = await headers(); const _headers = await headers();
@@ -57,6 +78,36 @@ export async function registerPasskeyLink(
sessionToken: sessionCookie.token, sessionToken: sessionCookie.token,
}); });
if (!session?.session?.factors?.user?.id) {
return { error: "Could not determine user from session" };
}
const sessionValid = isSessionValid(session.session);
if (!sessionValid) {
const authmethods = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.session.factors.user.id,
});
// if the user has no authmethods set, we need to check if the user was verified
if (authmethods.authMethodTypes.length !== 0) {
return {
error:
"You have to authenticate or have a valid User Verification Check",
};
}
// check if a verification was done earlier
const hasValidUserVerificationCheck = await checkUserVerification(
session.session.factors.user.id,
);
if (!hasValidUserVerificationCheck) {
return { error: "User Verification Check has to be done" };
}
}
const [hostname, port] = host.split(":"); const [hostname, port] = host.split(":");
if (!hostname) { if (!hostname) {

View File

@@ -37,6 +37,7 @@ import {
checkEmailVerification, checkEmailVerification,
checkMFAFactors, checkMFAFactors,
checkPasswordChangeRequired, checkPasswordChangeRequired,
checkUserVerification,
} from "../verify-helper"; } from "../verify-helper";
type ResetPasswordCommand = { type ResetPasswordCommand = {
@@ -297,6 +298,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
return { redirect: url }; return { redirect: url };
} }
// this function lets users with code set a password or users with valid User Verification Check
export async function changePassword(command: { export async function changePassword(command: {
code?: string; code?: string;
userId: string; userId: string;
@@ -316,11 +318,39 @@ export async function changePassword(command: {
} }
const userId = user.userId; const userId = user.userId;
if (user.state === UserState.INITIAL) {
return { error: "User Initial State is not supported" };
}
// check if the user has no password set in order to set a password
if (!command.code) {
const authmethods = await listAuthenticationMethodTypes({
serviceUrl,
userId,
});
// if the user has no authmethods set, we need to check if the user was verified
if (authmethods.authMethodTypes.length !== 0) {
return {
error:
"You have to provide a code or have a valid User Verification Check",
};
}
// check if a verification was done earlier
const hasValidUserVerificationCheck = await checkUserVerification(
user.userId,
);
if (!hasValidUserVerificationCheck) {
return { error: "User Verification Check has to be done" };
}
}
return setUserPassword({ return setUserPassword({
serviceUrl, serviceUrl,
userId, userId,
password: command.password, password: command.password,
user,
code: command.code, code: command.code,
}); });
} }

View File

@@ -1,24 +1,25 @@
"use server"; "use server";
import { import {
createInviteCode,
getLoginSettings, getLoginSettings,
getSession, getSession,
getUserByID, getUserByID,
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
resendEmailCode,
resendInviteCode,
verifyEmail, verifyEmail,
verifyInviteCode, verifyInviteCode,
verifyTOTPRegistration, verifyTOTPRegistration,
sendEmailCode as zitadelSendEmailCode, sendEmailCode as zitadelSendEmailCode,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import crypto from "crypto";
import { create } from "@zitadel/client"; 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 { cookies, headers } from "next/headers";
import { headers } from "next/headers";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getSessionCookieByLoginName } from "../cookies"; import { getSessionCookieByLoginName } from "../cookies";
import { getOrSetFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { loadMostRecentSession } from "../session"; import { loadMostRecentSession } from "../session";
import { checkMFAFactors } from "../verify-helper"; import { checkMFAFactors } from "../verify-helper";
@@ -69,14 +70,16 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
serviceUrl, serviceUrl,
userId: command.userId, userId: command.userId,
verificationCode: command.code, verificationCode: command.code,
}).catch(() => { }).catch((error) => {
console.warn(error);
return { error: "Could not verify invite" }; return { error: "Could not verify invite" };
}) })
: await verifyEmail({ : await verifyEmail({
serviceUrl, serviceUrl,
userId: command.userId, userId: command.userId,
verificationCode: command.code, verificationCode: command.code,
}).catch(() => { }).catch((error) => {
console.warn(error);
return { error: "Could not verify email" }; return { error: "Could not verify email" };
}); });
@@ -89,45 +92,6 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
} }
let session: Session | undefined; 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({
serviceUrl,
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({
serviceUrl,
userId: session?.factors?.user?.id,
});
if (!userResponse?.user) {
return { error: "Could not load user" };
}
user = userResponse.user;
} else {
const userResponse = await getUserByID({ const userResponse = await getUserByID({
serviceUrl, serviceUrl,
userId: command.userId, userId: command.userId,
@@ -137,40 +101,29 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
return { error: "Could not load user" }; return { error: "Could not load user" };
} }
user = userResponse.user; const user = userResponse.user;
const checks = create(ChecksSchema, { const sessionCookie = await getSessionCookieByLoginName({
user: { loginName:
search: { "loginName" in command ? command.loginName : user.preferredLoginName,
case: "loginName", organization: command.organization,
value: userResponse.user.preferredLoginName, }).catch((error) => {
}, console.warn("Ignored error:", error); // checked later
},
}); });
session = await createSessionAndUpdateCookie({ if (sessionCookie) {
checks, session = await getSession({
requestId: command.requestId,
});
}
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 loginSettings = await getLoginSettings({
serviceUrl, serviceUrl,
organization: user.details?.resourceOwner, sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
}).then((response) => {
if (response?.session) {
return response.session;
}
}); });
}
// load auth methods for user
const authMethodResponse = await listAuthenticationMethodTypes({ const authMethodResponse = await listAuthenticationMethodTypes({
serviceUrl, serviceUrl,
userId: user.userId, userId: user.userId,
@@ -186,6 +139,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
authMethodResponse.authMethodTypes && authMethodResponse.authMethodTypes &&
authMethodResponse.authMethodTypes.length == 0 authMethodResponse.authMethodTypes.length == 0
) { ) {
if (!sessionCookie) {
const checks = create(ChecksSchema, {
user: {
search: {
case: "loginName",
value: userResponse.user.preferredLoginName,
},
},
});
session = await createSessionAndUpdateCookie({
checks,
requestId: command.requestId,
});
}
if (!session) {
return { error: "Could not create session" };
}
const params = new URLSearchParams({ const params = new URLSearchParams({
sessionId: session.id, sessionId: session.id,
}); });
@@ -193,9 +166,62 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
if (session.factors?.user?.loginName) { if (session.factors?.user?.loginName) {
params.set("loginName", session.factors?.user?.loginName); params.set("loginName", session.factors?.user?.loginName);
} }
// set hash of userId and userAgentId to prevent attacks, checks are done for users with invalid sessions and invalid userAgentId
const cookiesList = await cookies();
const userAgentId = await getOrSetFingerprintId();
const verificationCheck = crypto
.createHash("sha256")
.update(`${user.userId}:${userAgentId}`)
.digest("hex");
await cookiesList.set({
name: "verificationCheck",
value: verificationCheck,
httpOnly: true,
path: "/",
maxAge: 300, // 5 minutes
});
return { redirect: `/authenticator/set?${params}` }; return { redirect: `/authenticator/set?${params}` };
} }
// if no session found only show success page,
// if user is invited, recreate invite flow to not depend on session
if (!session?.factors?.user?.id) {
const verifySuccessParams = new URLSearchParams({});
if (command.userId) {
verifySuccessParams.set("userId", command.userId);
}
if (
("loginName" in command && command.loginName) ||
user.preferredLoginName
) {
verifySuccessParams.set(
"loginName",
"loginName" in command && command.loginName
? command.loginName
: user.preferredLoginName,
);
}
if (command.requestId) {
verifySuccessParams.set("requestId", command.requestId);
}
if (command.organization) {
verifySuccessParams.set("organization", command.organization);
}
return { redirect: `/verify/success?${verifySuccessParams}` };
}
const loginSettings = await getLoginSettings({
serviceUrl,
organization: user.details?.resourceOwner,
});
// redirect to mfa factor if user has one, or redirect to set one up // redirect to mfa factor if user has one, or redirect to set one up
const mfaFactorCheck = await checkMFAFactors( const mfaFactorCheck = await checkMFAFactors(
serviceUrl, serviceUrl,
@@ -254,193 +280,50 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
return command.isInvite return command.isInvite
? resendInviteCode({ serviceUrl, userId: command.userId }) ? createInviteCode({
: resendEmailCode({ serviceUrl,
userId: command.userId,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(command.requestId ? `&requestId=${command.requestId}` : ""),
}).catch((error) => {
if (error.code === 9) {
return { error: "User is already verified!" };
}
return { error: "Could not resend invite" };
})
: zitadelSendEmailCode({
userId: command.userId, userId: command.userId,
serviceUrl, serviceUrl,
urlTemplate: urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(command.requestId ? `&requestId=${command.requestId}` : ""), (command.requestId ? `&requestId=${command.requestId}` : ""),
}); });
} }
type sendEmailCommand = { type SendEmailCommand = {
serviceUrl: string;
userId: string; userId: string;
urlTemplate: string; urlTemplate: string;
}; };
export async function sendEmailCode(command: sendEmailCommand) { export async function sendEmailCode(command: SendEmailCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
return zitadelSendEmailCode({ return zitadelSendEmailCode({
serviceUrl: command.serviceUrl, serviceUrl,
userId: command.userId, userId: command.userId,
urlTemplate: command.urlTemplate, urlTemplate: command.urlTemplate,
}); });
} }
export type SendVerificationRedirectWithoutCheckCommand = { export async function sendInviteEmailCode(command: SendEmailCommand) {
organization?: string;
requestId?: string;
} & (
| { userId: string; loginName?: never }
| { userId?: never; loginName: string }
);
export async function sendVerificationRedirectWithoutCheck(
command: SendVerificationRedirectWithoutCheckCommand,
) {
const _headers = await headers(); const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
if (!("loginName" in command || "userId" in command)) { return createInviteCode({
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({
serviceUrl,
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({
serviceUrl,
userId: 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({
serviceUrl, serviceUrl,
userId: command.userId, userId: command.userId,
}); urlTemplate: command.urlTemplate,
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,
requestId: command.requestId,
}); });
} }
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({
serviceUrl,
userId: 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({
serviceUrl,
organization: user.details?.resourceOwner,
});
// redirect to mfa factor if user has one, or redirect to set one up
const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
session,
loginSettings,
authMethodResponse.authMethodTypes,
command.organization,
command.requestId,
);
if (mfaFactorCheck?.redirect) {
return mfaFactorCheck;
}
// login user if no additional steps are required
if (command.requestId && session.id) {
const nextUrl = await getNextUrl(
{
sessionId: session.id,
requestId: command.requestId,
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 };
}

View File

@@ -4,7 +4,10 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings
import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import crypto from "crypto";
import moment from "moment"; import moment from "moment";
import { cookies } from "next/headers";
import { getFingerprintIdCookie } from "./fingerprint";
import { getUserByID } from "./zitadel"; import { getUserByID } from "./zitadel";
export function checkPasswordChangeRequired( export function checkPasswordChangeRequired(
@@ -44,7 +47,7 @@ export function checkPasswordChangeRequired(
} }
} }
export function checkInvite( export function checkEmailVerified(
session: Session, session: Session,
humanUser?: HumanUser, humanUser?: HumanUser,
organization?: string, organization?: string,
@@ -54,7 +57,7 @@ export function checkInvite(
const paramsVerify = new URLSearchParams({ const paramsVerify = new URLSearchParams({
loginName: session.factors?.user?.loginName as string, loginName: session.factors?.user?.loginName as string,
userId: session.factors?.user?.id as string, // verify needs user id userId: session.factors?.user?.id as string, // verify needs user id
invite: "true", // TODO: check - set this to true as we dont expect old email verification method here send: "true", // we request a new email code once the page is loaded
}); });
if (organization || session.factors?.user?.organizationId) { if (organization || session.factors?.user?.organizationId) {
@@ -84,6 +87,7 @@ export function checkEmailVerification(
) { ) {
const params = new URLSearchParams({ const params = new URLSearchParams({
loginName: session.factors?.user?.loginName as string, loginName: session.factors?.user?.loginName as string,
send: "true", // set this to true as we dont expect old email codes to be valid anymore
}); });
if (requestId) { if (requestId) {
@@ -248,3 +252,38 @@ export async function checkMFAFactors(
return { redirect: `/mfa/set?` + params }; return { redirect: `/mfa/set?` + params };
} }
} }
export async function checkUserVerification(userId: string): Promise<boolean> {
// check if a verification was done earlier
const cookiesList = await cookies();
// only read cookie to prevent issues on page.tsx
const fingerPrintCookie = await getFingerprintIdCookie();
if (!fingerPrintCookie || !fingerPrintCookie.value) {
return false;
}
const verificationCheck = crypto
.createHash("sha256")
.update(`${userId}:${fingerPrintCookie.value}`)
.digest("hex");
const cookieValue = await cookiesList.get("verificationCheck")?.value;
if (!cookieValue) {
console.warn(
"User verification check cookie not found. User verification check failed.",
);
return false;
}
if (cookieValue !== verificationCheck) {
console.warn(
`User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`,
);
return false;
}
return true;
}

View File

@@ -29,11 +29,7 @@ import {
SearchQuery, SearchQuery,
SearchQuerySchema, SearchQuerySchema,
} from "@zitadel/proto/zitadel/user/v2/query_pb"; } from "@zitadel/proto/zitadel/user/v2/query_pb";
import { import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
SendInviteCodeSchema,
User,
UserState,
} from "@zitadel/proto/zitadel/user/v2/user_pb";
import { import {
AddHumanUserRequest, AddHumanUserRequest,
ResendEmailCodeRequest, ResendEmailCodeRequest,
@@ -506,21 +502,6 @@ export async function verifyInviteCode({
return userService.verifyInviteCode({ userId, verificationCode }, {}); return userService.verifyInviteCode({ userId, verificationCode }, {});
} }
export async function resendInviteCode({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
UserService,
serviceUrl,
);
return userService.resendInviteCode({ userId }, {});
}
export async function sendEmailCode({ export async function sendEmailCode({
serviceUrl, serviceUrl,
userId, userId,
@@ -1170,13 +1151,11 @@ export async function setUserPassword({
serviceUrl, serviceUrl,
userId, userId,
password, password,
user,
code, code,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
password: string; password: string;
user: User;
code?: string; code?: string;
}) { }) {
let payload = create(SetPasswordRequestSchema, { let payload = create(SetPasswordRequestSchema, {
@@ -1186,22 +1165,6 @@ export async function setUserPassword({
}, },
}); });
// check if the user has no password set in order to set a password
if (!code) {
const authmethods = await listAuthenticationMethodTypes({
serviceUrl,
userId,
});
// if the user has no authmethods set, we can set a password otherwise we need a code
if (
!(authmethods.authMethodTypes.length === 0) &&
user.state !== UserState.INITIAL
) {
return { error: "Provide a code to set a password" };
}
}
if (code) { if (code) {
payload = { payload = {
...payload, ...payload,