"use server"; import { createLogger } from "@/lib/logger"; import { createInviteCode, getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, verifyEmail, verifyInviteCode, verifyTOTPRegistration, sendEmailCode as zitadelSendEmailCode, } from "@/lib/zitadel"; import crypto from "crypto"; import { create } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getTranslations } from "next-intl/server"; import { cookies, headers } from "next/headers"; import { completeFlowOrGetUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; import { getOrSetFingerprintId } from "../fingerprint"; import { getServiceConfig } from "../service-url"; import { loadMostRecentSession } from "../session"; import { checkMFAFactors } from "../verify-helper"; import { createSessionAndUpdateCookie } from "./cookie"; import { getPublicHostWithProtocol } from "./host"; const logger = createLogger("verify"); export async function verifyTOTP(code: string, loginName?: string, organization?: string) { const _headers = await headers(); const { serviceConfig } = getServiceConfig(_headers); return loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization, }, }).then((session) => { if (session?.factors?.user?.id) { return verifyTOTPRegistration({ serviceConfig, code, userId: session.factors.user.id }); } else { throw Error("No user id found in session."); } }); } type VerifyUserByEmailCommand = { userId: string; loginName?: string; // to determine already existing session organization?: string; code: string; isInvite: boolean; requestId?: string; }; export async function sendVerification(command: VerifyUserByEmailCommand) { const t = await getTranslations("verify"); const _headers = await headers(); const { serviceConfig } = getServiceConfig(_headers); const verifyResponse = command.isInvite ? await verifyInviteCode({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => { logger.warn("Could not verify invite:", { error }); return { error: t("errors.couldNotVerifyInvite") }; }) : await verifyEmail({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => { logger.warn("Could not verify email:", { error }); return { error: t("errors.couldNotVerifyEmail") }; }); if ("error" in verifyResponse) { return verifyResponse; } if (!verifyResponse) { return { error: t("errors.couldNotVerify") }; } let session: Session | undefined; const userResponse = await getUserByID({ serviceConfig, userId: command.userId }); if (!userResponse || !userResponse.user) { return { error: t("errors.couldNotLoadUser") }; } const user = userResponse.user; const sessionCookie = await getSessionCookieByLoginName({ loginName: "loginName" in command ? command.loginName : user.preferredLoginName, organization: command.organization, }); if (sessionCookie) { session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token }) .then((response) => { if (response?.session) { return response.session; } }) .catch((error) => { // user session is not found, so we create a new one logger.warn("[verify] user session is not found, so we create a new one", { error }); return undefined; }); } // load auth methods for user const authMethodResponse = await listAuthenticationMethodTypes({ serviceConfig, userId: user.userId }); if (!authMethodResponse || !authMethodResponse.authMethodTypes) { return { error: t("errors.couldNotLoadAuthenticators") }; } const hasPrimaryMethod = authMethodResponse?.authMethodTypes?.some( (m: AuthenticationMethodType) => m === AuthenticationMethodType.PASSWORD || m === AuthenticationMethodType.PASSKEY || m === AuthenticationMethodType.IDP, ) ?? false; // if no primary auth methods are found on the user, redirect to set one up if (!hasPrimaryMethod) { if (!session) { const checks = create(ChecksSchema, { user: { search: { case: "loginName", value: userResponse.user.preferredLoginName, }, }, }); const result = await createSessionAndUpdateCookie({ checks, requestId: command.requestId, }); session = result.session; } if (!session) { return { error: t("errors.couldNotCreateSession") }; } const params = new URLSearchParams({ sessionId: session.id, }); if (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}` }; } // 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({ serviceConfig, organization: user.details?.resourceOwner }); // redirect to mfa factor if user has one, or redirect to set one up const mfaFactorCheck = await checkMFAFactors( serviceConfig, 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) { return completeFlowOrGetUrl( { sessionId: session.id, requestId: command.requestId, organization: command.organization ?? session.factors?.user?.organizationId, }, loginSettings?.defaultRedirectUri, ); } // Regular flow - return URL for client-side navigation return completeFlowOrGetUrl( { loginName: session.factors.user.loginName, organization: session.factors?.user?.organizationId, }, loginSettings?.defaultRedirectUri, ); } type resendVerifyEmailCommand = { userId: string; isInvite: boolean; requestId?: string; }; export async function resendVerification(command: resendVerifyEmailCommand) { const t = await getTranslations("verify"); const _headers = await headers(); const { serviceConfig } = getServiceConfig(_headers); const hostWithProtocol = await getPublicHostWithProtocol(_headers); const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; return command.isInvite ? createInviteCode({ serviceConfig, userId: command.userId, urlTemplate: `${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + (command.requestId ? `&requestId=${command.requestId}` : ""), }).catch((error) => { if (error.code === 9) { return { error: t("errors.userAlreadyVerified") }; } return { error: t("errors.couldNotResendInvite") }; }) : zitadelSendEmailCode({ serviceConfig, userId: command.userId, urlTemplate: `${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (command.requestId ? `&requestId=${command.requestId}` : ""), }); } type SendEmailCommand = { userId: string; urlTemplate: string; }; export async function sendEmailCode(command: SendEmailCommand) { const _headers = await headers(); const { serviceConfig } = getServiceConfig(_headers); return zitadelSendEmailCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate }); } export async function sendInviteEmailCode(command: SendEmailCommand) { const _headers = await headers(); const { serviceConfig } = getServiceConfig(_headers); return createInviteCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate }); }