From eea139eca608b4f9d502eb8fb9f06308bc65867e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 10:55:53 +0200 Subject: [PATCH 1/4] recheck for valid user verification on /authenticator/set remove check on the /verify page itself --- .../app/(login)/authenticator/set/page.tsx | 37 +++++++++++++++++-- apps/login/src/app/(login)/verify/page.tsx | 5 +-- .../src/components/verify-redirect-button.tsx | 14 ++++++- apps/login/src/lib/server/verify.ts | 6 ++- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 3e1b49eed0..63704b87eb 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -7,6 +7,7 @@ import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; +import { checkUserVerification } from "@/lib/verify-helper"; import { getActiveIdentityProviders, getBrandingSettings, @@ -18,6 +19,7 @@ import { import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; +import { redirect } from "next/navigation"; export default async function Page(props: { searchParams: Promise>; @@ -92,20 +94,49 @@ export default async function Page(props: { }); } - if (!sessionWithData) { + if ( + !sessionWithData || + !sessionWithData.factors || + !sessionWithData.factors.user + ) { return {tError("unknownContext")}; } const branding = await getBrandingSettings({ serviceUrl, - organization: sessionWithData.factors?.user?.organizationId, + organization: sessionWithData.factors.user?.organizationId, }); const loginSettings = await getLoginSettings({ 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, + 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({ serviceUrl, orgId: sessionWithData.factors?.user?.organizationId, diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index fcbc4fd34a..c9136b4669 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,7 +6,6 @@ import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; -import { checkUserVerification } from "@/lib/verify-helper"; import { getBrandingSettings, getUserByID, @@ -112,8 +111,6 @@ export default async function Page(props: { searchParams: Promise }) { } } - const hasValidUserVerificationCheck = await checkUserVerification(id); - const params = new URLSearchParams({ userId: userId, initial: "true", // defines that a code is not required and is therefore not shown in the UI @@ -172,7 +169,7 @@ export default async function Page(props: { searchParams: Promise }) { )} {/* show a button to setup auth method for the user otherwise show the UI for reverifying */} - {human?.email?.isVerified && hasValidUserVerificationCheck ? ( + {human?.email?.isVerified ? ( // show page for already verified users (""); const [loading, setLoading] = useState(false); + const router = useRouter(); async function submitAndContinue(): Promise { setLoading(true); @@ -50,7 +52,7 @@ export function VerifyRedirectButton({ } as SendVerificationRedirectWithoutCheckCommand; } - await sendVerificationRedirectWithoutCheck(command) + const response = await sendVerificationRedirectWithoutCheck(command) .catch(() => { setError("Could not verify"); return; @@ -58,6 +60,16 @@ export function VerifyRedirectButton({ .finally(() => { setLoading(false); }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + router.push(response.redirect); + return true; + } } return ( diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 0ab4c5465d..34176a45e6 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -71,14 +71,16 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { serviceUrl, userId: command.userId, verificationCode: command.code, - }).catch(() => { + }).catch((error) => { + console.warn(error); return { error: "Could not verify invite" }; }) : await verifyEmail({ serviceUrl, userId: command.userId, verificationCode: command.code, - }).catch(() => { + }).catch((error) => { + console.warn(error); return { error: "Could not verify email" }; }); From 0c6972d068e9c3fed8d0fee9530b22643a2aace2 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 11:57:44 +0200 Subject: [PATCH 2/4] cleanup verification logic --- apps/login/src/app/(login)/verify/page.tsx | 49 +++++----------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index c9136b4669..121e250dc3 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -2,17 +2,11 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; -import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; -import { - getBrandingSettings, - getUserByID, - listAuthenticationMethodTypes, -} from "@/lib/zitadel"; +import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; 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 { headers } from "next/headers"; @@ -100,17 +94,6 @@ export default async function Page(props: { searchParams: Promise }) { throw Error("Failed to get user id"); } - let authMethods: AuthenticationMethodType[] | null = null; - if (human?.email?.isVerified) { - const authMethodsResponse = await listAuthenticationMethodTypes({ - serviceUrl, - userId, - }); - if (authMethodsResponse.authMethodTypes) { - authMethods = authMethodsResponse.authMethodTypes; - } - } - const params = new URLSearchParams({ userId: userId, initial: "true", // defines that a code is not required and is therefore not shown in the UI @@ -168,27 +151,15 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {/* show a button to setup auth method for the user otherwise show the UI for reverifying */} - {human?.email?.isVerified ? ( - // show page for already verified users - - ) : ( - // check if auth methods are set - - )} + {/* always show the code form / TODO improve UI for email links which were already used (currently we get an error code 3 due being reused) */} + ); From 446768b5e4855f8651714dec620eacc2c8adb345 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 12:05:06 +0200 Subject: [PATCH 3/4] fix invite resend --- apps/login/src/lib/server/verify.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 34176a45e6..d4c5e889fe 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -1,11 +1,11 @@ "use server"; import { - createInviteCode, getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, + resendInviteCode, verifyEmail, verifyInviteCode, verifyTOTPRegistration, @@ -275,14 +275,10 @@ export async function resendVerification(command: resendVerifyEmailCommand) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - // create a new invite whenever the resend is called return command.isInvite - ? createInviteCode({ + ? resendInviteCode({ 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}` : ""), }) //resendInviteCode({ serviceUrl, userId: command.userId }) : sendEmailCode({ userId: command.userId, From a1a6326fae24e084e2602f455d745bd2a7eb28af Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 12:05:18 +0200 Subject: [PATCH 4/4] cleanup --- apps/login/src/lib/server/verify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index d4c5e889fe..88bb0139b3 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -279,7 +279,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) { ? resendInviteCode({ serviceUrl, userId: command.userId, - }) //resendInviteCode({ serviceUrl, userId: command.userId }) + }) : sendEmailCode({ userId: command.userId, serviceUrl,