From 575831f2528306ca4f6d12bb92aec5d678600823 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 19 May 2025 14:36:26 +0200 Subject: [PATCH 01/29] set cookie --- apps/login/src/lib/server/verify.ts | 25 ++++++++++++++++++++++++- apps/login/src/lib/zitadel.ts | 23 ++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index e7c9f5e715..d56af5e582 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -12,13 +12,16 @@ import { 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 { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { headers } from "next/headers"; +import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; +import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { loadMostRecentSession } from "../session"; import { checkMFAFactors } from "../verify-helper"; @@ -193,6 +196,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { if (session.factors?.user?.loginName) { params.set("loginName", session.factors?.user?.loginName); } + + // set hash of userId and userAgentId to prevent replay attacks, TODO: check on the /authenticator/set page + + const cookiesList = await cookies(); + + const userAgentId = await getFingerprintId(); + + 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}` }; } diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 508baf1667..409e63e6cf 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -45,8 +45,10 @@ import { VerifyPasskeyRegistrationRequest, VerifyU2FRegistrationRequest, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; import { unstable_cacheLife as cacheLife } from "next/cache"; -import { getUserAgent } from "./fingerprint"; +import { cookies } from "next/headers"; +import { getFingerprintId, getUserAgent } from "./fingerprint"; import { createServiceForHost } from "./service"; const useCache = process.env.DEBUG !== "true"; @@ -1198,6 +1200,25 @@ export async function setUserPassword({ !(authmethods.authMethodTypes.length === 0) && user.state !== UserState.INITIAL ) { + // check if a verification was done earlier + + const cookiesList = await cookies(); + + const userAgentId = await getFingerprintId(); + + 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 { error: "Provide a code to set a password" }; } } From 1ffb9968158a0d8db2a51fefb091f3042cda559c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 May 2025 09:52:59 +0200 Subject: [PATCH 02/29] improve password handling --- .../login/src/components/register-passkey.tsx | 11 ++ apps/login/src/lib/server/passkeys.ts | 44 +++++- apps/login/src/lib/server/password.ts | 135 +++++++++--------- apps/login/src/lib/zitadel.ts | 47 +----- 4 files changed, 124 insertions(+), 113 deletions(-) diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx index 163ab507b8..8687312bbc 100644 --- a/apps/login/src/components/register-passkey.tsx +++ b/apps/login/src/components/register-passkey.tsx @@ -83,6 +83,16 @@ export function RegisterPasskey({ 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 options: CredentialCreationOptions = (resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? @@ -92,6 +102,7 @@ export function RegisterPasskey({ setError("An error on registering passkey"); return; } + options.publicKey.challenge = coerceToArrayBuffer( options.publicKey.challenge, "challenge", diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 73d12043b0..c5ad990c61 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -5,6 +5,7 @@ import { getLoginSettings, getSession, getUserByID, + listAuthenticationMethodTypes, registerPasskey, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, } from "@/lib/zitadel"; @@ -14,7 +15,8 @@ import { RegisterPasskeyResponse, VerifyPasskeyRegistrationRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { headers } from "next/headers"; +import crypto from "crypto"; +import { cookies, headers } from "next/headers"; import { userAgent } from "next/server"; import { getNextUrl } from "../client"; import { @@ -22,6 +24,7 @@ import { getSessionCookieById, getSessionCookieByLoginName, } from "../cookies"; +import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification } from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; @@ -39,7 +42,7 @@ type RegisterPasskeyCommand = { export async function registerPasskeyLink( command: RegisterPasskeyCommand, -): Promise { +): Promise { const { sessionId } = command; const _headers = await headers(); @@ -57,6 +60,43 @@ export async function registerPasskeyLink( sessionToken: sessionCookie.token, }); + if (!session?.session?.factors?.user?.id) { + return { error: "Could not determine user from session" }; + } + + 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 + // users are redirected from /authenticator/set to /password/set + 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 cookiesList = await cookies(); + const userAgentId = await getFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${user.userId}:${userAgentId}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + return { error: "User Verification Check has to be done" }; + } + + if (cookieValue !== verificationCheck) { + return { error: "User Verification Check has to be done" }; + } + const [hostname, port] = host.split(":"); if (!hostname) { diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 9e256c71d9..26adb56c7b 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -13,7 +13,6 @@ import { listAuthenticationMethodTypes, listUsers, passwordReset, - setPassword, setUserPassword, } from "@/lib/zitadel"; import { ConnectError, create } from "@zitadel/client"; @@ -25,13 +24,12 @@ import { } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { - AuthenticationMethodType, - SetPasswordRequestSchema, -} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { headers } from "next/headers"; +import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; +import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; +import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification, @@ -297,6 +295,7 @@ export async function sendPassword(command: UpdateSessionCommand) { return { redirect: url }; } +// this function lets users with code set a password or users with valid User Verification Check export async function changePassword(command: { code?: string; userId: string; @@ -316,11 +315,50 @@ export async function changePassword(command: { } 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 + // users are redirected from /authenticator/set to /password/set + 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 cookiesList = await cookies(); + const userAgentId = await getFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${user.userId}:${userAgentId}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + return { error: "User Verification Check has to be done" }; + } + + if (cookieValue !== verificationCheck) { + return { error: "User Verification Check has to be done" }; + } + } + return setUserPassword({ serviceUrl, userId, password: command.password, - user, code: command.code, }); } @@ -366,67 +404,32 @@ export async function checkSessionAndSetPassword({ return { error: "Could not load auth methods" }; } - const requiredAuthMethodsForForceMFA = [ - AuthenticationMethodType.OTP_EMAIL, - AuthenticationMethodType.OTP_SMS, - AuthenticationMethodType.TOTP, - AuthenticationMethodType.U2F, - ]; - - const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every( - (method) => !authmethods.authMethodTypes.includes(method), - ); - - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: session.factors.user.organizationId, - }); - - const forceMfa = !!( - loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly - ); - - // if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user - if (forceMfa && hasNoMFAMethods) { - return setPassword({ serviceUrl, payload }).catch((error) => { - // throw error if failed precondition (ex. User is not yet initialized) - if (error.code === 9 && error.message) { - return { error: "Failed precondition" }; - } else { - throw error; - } + const transport = async (serviceUrl: string, token: string) => { + return createServerTransport(token, { + baseUrl: serviceUrl, }); - } else { - const transport = async (serviceUrl: string, token: string) => { - return createServerTransport(token, { - baseUrl: serviceUrl, - }); - }; + }; - const myUserService = async (serviceUrl: string, sessionToken: string) => { - const transportPromise = await transport(serviceUrl, sessionToken); - return createUserServiceClient(transportPromise); - }; + const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await transport(serviceUrl, sessionToken); + return createUserServiceClient(transportPromise); + }; - const selfService = await myUserService( - serviceUrl, - `${sessionCookie.token}`, - ); + const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`); - return selfService - .setPassword( - { - userId: session.factors.user.id, - newPassword: { password, changeRequired: false }, - }, - {}, - ) - .catch((error: ConnectError) => { - console.log(error); - if (error.code === 7) { - return { error: "Session is not valid." }; - } - throw error; - }); - } + return selfService + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error: ConnectError) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); } diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 409e63e6cf..d1fe83434d 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -29,11 +29,7 @@ import { SearchQuery, SearchQuerySchema, } from "@zitadel/proto/zitadel/user/v2/query_pb"; -import { - SendInviteCodeSchema, - User, - UserState, -} from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AddHumanUserRequest, ResendEmailCodeRequest, @@ -45,10 +41,8 @@ import { VerifyPasskeyRegistrationRequest, VerifyU2FRegistrationRequest, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; import { unstable_cacheLife as cacheLife } from "next/cache"; -import { cookies } from "next/headers"; -import { getFingerprintId, getUserAgent } from "./fingerprint"; +import { getUserAgent } from "./fingerprint"; import { createServiceForHost } from "./service"; const useCache = process.env.DEBUG !== "true"; @@ -1172,13 +1166,11 @@ export async function setUserPassword({ serviceUrl, userId, password, - user, code, }: { serviceUrl: string; userId: string; password: string; - user: User; code?: string; }) { let payload = create(SetPasswordRequestSchema, { @@ -1188,41 +1180,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 - ) { - // check if a verification was done earlier - - const cookiesList = await cookies(); - - const userAgentId = await getFingerprintId(); - - 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 { error: "Provide a code to set a password" }; - } - } - if (code) { payload = { ...payload, From 93b333837d82794a908b02f431c03e2dfd0d2882 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 May 2025 14:10:18 +0200 Subject: [PATCH 03/29] helper functions --- apps/login/src/app/(login)/verify/page.tsx | 54 +++++++++------- apps/login/src/lib/server/loginname.ts | 28 +++++++- apps/login/src/lib/server/passkeys.ts | 75 +++++++++++++--------- apps/login/src/lib/server/password.ts | 24 ++----- apps/login/src/lib/server/verify.ts | 8 +-- apps/login/src/lib/verify-helper.ts | 34 +++++++++- 6 files changed, 143 insertions(+), 80 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 198a46a5fe..5d7941322a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,6 +6,7 @@ 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, @@ -96,14 +97,23 @@ export default async function Page(props: { searchParams: Promise }) { id = userId ?? sessionFactors?.factors?.user?.id; + if (!id) { + throw Error("Failed to get user id"); + } + let authMethods: AuthenticationMethodType[] | null = null; if (human?.email?.isVerified) { - const authMethodsResponse = await listAuthenticationMethodTypes(userId); + const authMethodsResponse = await listAuthenticationMethodTypes({ + serviceUrl, + userId, + }); if (authMethodsResponse.authMethodTypes) { authMethods = authMethodsResponse.authMethodTypes; } } + 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 @@ -155,27 +165,27 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {id && - (human?.email?.isVerified ? ( - // show page for already verified users - - ) : ( - // check if auth methods are set - - ))} + {/* show a button to setup auth method for the user otherwise show the UI for reverifying */} + {human?.email?.isVerified && hasValidUserVerificationCheck ? ( + // show page for already verified users + + ) : ( + // check if auth methods are set + + )} ); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 2ea6004fdc..8754e77d56 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,7 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkInvite } from "../verify-helper"; +import { checkEmailVerified, checkUserVerification } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, @@ -257,7 +257,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { // this can be expected to be an invite as users created in console have a password set. if (!methods.authMethodTypes || !methods.authMethodTypes.length) { // redirect to /verify invite if no auth method is set and email is not verified - const inviteCheck = checkInvite( + const inviteCheck = checkEmailVerified( session, humanUser, session.factors.user.organizationId, @@ -268,6 +268,30 @@ export async function sendLoginname(command: SendLoginnameCommand) { return inviteCheck; } + // check if user was verified + const isUserVerified = await checkUserVerification( + session.factors.user.id, + ); + if (!isUserVerified) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (command.requestId) { + params.append("requestId", command.requestId); + } + + if (command.organization || session.factors?.user?.organizationId) { + params.append( + "organization", + command.organization ?? + (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/verify?` + params }; + } + const paramsAuthenticatorSetup = new URLSearchParams({ loginName: session.factors?.user?.loginName, userId: session.factors?.user?.id, // verify needs user id diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index c5ad990c61..3470629f24 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -9,14 +9,14 @@ import { registerPasskey, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, } 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 { RegisterPasskeyResponse, VerifyPasskeyRegistrationRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; -import { cookies, headers } from "next/headers"; +import { headers } from "next/headers"; import { userAgent } from "next/server"; import { getNextUrl } from "../client"; import { @@ -24,9 +24,11 @@ import { getSessionCookieById, getSessionCookieByLoginName, } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkEmailVerification } from "../verify-helper"; +import { + checkEmailVerification, + checkUserVerification, +} from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { @@ -40,6 +42,22 @@ type RegisterPasskeyCommand = { sessionId: string; }; +function isSessionValid(session: Partial): { + 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( command: RegisterPasskeyCommand, ): Promise { @@ -64,37 +82,30 @@ export async function registerPasskeyLink( return { error: "Could not determine user from session" }; } - const authmethods = await listAuthenticationMethodTypes({ - serviceUrl, - userId: session?.session?.factors?.user?.id, - }); + const sessionValid = isSessionValid(session.session); - // if the user has no authmethods set, we need to check if the user was verified - // users are redirected from /authenticator/set to /password/set - if (authmethods.authMethodTypes.length !== 0) { - return { - error: - "You have to provide a code or have a valid User Verification Check", - }; - } + if (!sessionValid) { + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.session.factors.user.id, + }); - // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getFingerprintId(); + // 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", + }; + } - const verificationCheck = crypto - .createHash("sha256") - .update(`${user.userId}:${userAgentId}`) - .digest("hex"); + // check if a verification was done earlier + const hasValidUserVerificationCheck = await checkUserVerification( + session.session.factors.user.id, + ); - const cookieValue = await cookiesList.get("verificationCheck")?.value; - - if (!cookieValue) { - return { error: "User Verification Check has to be done" }; - } - - if (cookieValue !== verificationCheck) { - return { error: "User Verification Check has to be done" }; + if (!hasValidUserVerificationCheck) { + return { error: "User Verification Check has to be done" }; + } } const [hostname, port] = host.split(":"); diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 26adb56c7b..34859d419b 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -25,16 +25,15 @@ import { import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; -import { cookies, headers } from "next/headers"; +import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification, checkMFAFactors, checkPasswordChangeRequired, + checkUserVerification, } from "../verify-helper"; type ResetPasswordCommand = { @@ -327,7 +326,6 @@ export async function changePassword(command: { }); // if the user has no authmethods set, we need to check if the user was verified - // users are redirected from /authenticator/set to /password/set if (authmethods.authMethodTypes.length !== 0) { return { error: @@ -336,21 +334,11 @@ export async function changePassword(command: { } // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getFingerprintId(); + const hasValidUserVerificationCheck = await checkUserVerification( + user.userId, + ); - const verificationCheck = crypto - .createHash("sha256") - .update(`${user.userId}:${userAgentId}`) - .digest("hex"); - - const cookieValue = await cookiesList.get("verificationCheck")?.value; - - if (!cookieValue) { - return { error: "User Verification Check has to be done" }; - } - - if (cookieValue !== verificationCheck) { + if (!hasValidUserVerificationCheck) { return { error: "User Verification Check has to be done" }; } } diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index d56af5e582..db014431ac 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -21,7 +21,7 @@ import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; +import { getOrSetFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { loadMostRecentSession } from "../session"; import { checkMFAFactors } from "../verify-helper"; @@ -197,11 +197,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { params.set("loginName", session.factors?.user?.loginName); } - // set hash of userId and userAgentId to prevent replay attacks, TODO: check on the /authenticator/set page - + // 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 getFingerprintId(); + const userAgentId = await getOrSetFingerprintId(); const verificationCheck = crypto .createHash("sha256") diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 704d7bbef6..763f9dff45 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -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 { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; import moment from "moment"; +import { cookies } from "next/headers"; +import { getOrSetFingerprintId } from "./fingerprint"; import { getUserByID } from "./zitadel"; export function checkPasswordChangeRequired( @@ -44,7 +47,7 @@ export function checkPasswordChangeRequired( } } -export function checkInvite( +export function checkEmailVerified( session: Session, humanUser?: HumanUser, organization?: string, @@ -248,3 +251,32 @@ export async function checkMFAFactors( return { redirect: `/mfa/set?` + params }; } } + +export async function checkUserVerification(userId: string): Promise { + // check if a verification was done earlier + const cookiesList = await cookies(); + const userAgentId = await getOrSetFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${userId}:${userAgentId}`) + .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; +} From 82234da1f4aa98ef0ccb67a1edba1dc00a2f0726 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 May 2025 15:27:34 +0200 Subject: [PATCH 04/29] change send approach --- apps/login/cypress/integration/verify.cy.ts | 2 +- apps/login/src/app/(login)/verify/page.tsx | 8 ++++---- apps/login/src/lib/server/verify.ts | 18 ++++++++++++++---- apps/login/src/lib/verify-helper.ts | 3 ++- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/login/cypress/integration/verify.cy.ts b/apps/login/cypress/integration/verify.cy.ts index 464bf02e59..ab59b27363 100644 --- a/apps/login/cypress/integration/verify.cy.ts +++ b/apps/login/cypress/integration/verify.cy.ts @@ -90,7 +90,7 @@ describe("verify email", () => { }); // TODO: Avoid uncaught exception in application cy.once("uncaught:exception", () => false); - cy.visit("/verify?userId=221394658884845598&code=abc"); + cy.visit("/verify?userId=221394658884845598&code=abc&send=true"); cy.contains("Could not verify email", { timeout: 10_000 }); }); }); diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 5d7941322a..567774b0e4 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -23,7 +23,7 @@ export default async function Page(props: { searchParams: Promise }) { const t = await getTranslations({ locale, namespace: "verify" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { userId, loginName, code, organization, requestId, invite } = + const { userId, loginName, code, organization, requestId, invite, send } = searchParams; const _headers = await headers(); @@ -44,7 +44,7 @@ export default async function Page(props: { searchParams: Promise }) { let human: HumanUser | undefined; let id: string | undefined; - const doSend = invite !== "true"; + const doSend = send === "true"; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; @@ -62,7 +62,7 @@ export default async function Page(props: { searchParams: Promise }) { serviceUrl, userId: sessionFactors?.factors?.user?.id, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); @@ -75,7 +75,7 @@ export default async function Page(props: { searchParams: Promise }) { serviceUrl, userId, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index db014431ac..334f3b5e32 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -1,12 +1,12 @@ "use server"; import { + createInviteCode, getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, resendEmailCode, - resendInviteCode, verifyEmail, verifyInviteCode, verifyTOTPRegistration, @@ -274,14 +274,24 @@ export async function resendVerification(command: resendVerifyEmailCommand) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + const doSend = true; + // create a new invite whenever the resend is called return command.isInvite - ? resendInviteCode({ serviceUrl, userId: command.userId }) + ? createInviteCode({ + 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}` : "") + + (doSend ? `&send=${doSend}` : ""), + }) //resendInviteCode({ serviceUrl, userId: command.userId }) : resendEmailCode({ userId: command.userId, serviceUrl, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + - (command.requestId ? `&requestId=${command.requestId}` : ""), + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (command.requestId ? `&requestId=${command.requestId}` : "") + + (doSend ? `&send=${doSend}` : ""), }); } diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 763f9dff45..0385d62a03 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -57,7 +57,7 @@ export function checkEmailVerified( const paramsVerify = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, 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", // set this to true to request a new code immediately }); if (organization || session.factors?.user?.organizationId) { @@ -87,6 +87,7 @@ export function checkEmailVerification( ) { const params = new URLSearchParams({ 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) { From 2dac623c1e90d64f36b763a9918eaded8448a355 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 09:23:44 +0200 Subject: [PATCH 05/29] check email sending --- apps/login/cypress/integration/verify.cy.ts | 2 +- apps/login/locales/de.json | 1 + apps/login/locales/en.json | 1 + apps/login/locales/es.json | 1 + apps/login/locales/it.json | 1 + apps/login/locales/pl.json | 1 + apps/login/locales/ru.json | 1 + apps/login/locales/zh.json | 1 + apps/login/src/app/(login)/verify/page.tsx | 8 +++++++- apps/login/src/lib/server/loginname.ts | 3 ++- apps/login/src/lib/server/verify.ts | 8 ++------ 11 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/login/cypress/integration/verify.cy.ts b/apps/login/cypress/integration/verify.cy.ts index ab59b27363..464bf02e59 100644 --- a/apps/login/cypress/integration/verify.cy.ts +++ b/apps/login/cypress/integration/verify.cy.ts @@ -90,7 +90,7 @@ describe("verify email", () => { }); // TODO: Avoid uncaught exception in application cy.once("uncaught:exception", () => false); - cy.visit("/verify?userId=221394658884845598&code=abc&send=true"); + cy.visit("/verify?userId=221394658884845598&code=abc"); cy.contains("Could not verify email", { timeout: 10_000 }); }); }); diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index a2c137cf43..7471cc6ec7 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -181,6 +181,7 @@ "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.", "noCodeReceived": "Keinen Code erhalten?", "resendCode": "Code erneut senden", + "codeSent": "Ein Code wurde gerade an Ihre E-Mail-Adresse gesendet.", "submit": "Weiter" } }, diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 63a45c7d15..164761f43d 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -181,6 +181,7 @@ "description": "Enter the Code provided in the verification email.", "noCodeReceived": "Didn't receive a code?", "resendCode": "Resend code", + "codeSent": "A code has just been sent to your email address.", "submit": "Continue" } }, diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 60570eceb0..6e5dd43820 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -181,6 +181,7 @@ "description": "Introduce el código proporcionado en el correo electrónico de verificación.", "noCodeReceived": "¿No recibiste un código?", "resendCode": "Reenviar código", + "codeSent": "Se ha enviado un código a tu dirección de correo electrónico.", "submit": "Continuar" } }, diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 53894fdf5d..1173906e82 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -181,6 +181,7 @@ "description": "Inserisci il codice fornito nell'email di verifica.", "noCodeReceived": "Non hai ricevuto un codice?", "resendCode": "Invia di nuovo il codice", + "codeSent": "Un codice è stato appena inviato al tuo indirizzo email.", "submit": "Continua" } }, diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index 52b802eccb..ac3758227e 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -181,6 +181,7 @@ "description": "Wprowadź kod z wiadomości weryfikacyjnej.", "noCodeReceived": "Nie otrzymałeś kodu?", "resendCode": "Wyślij kod ponownie", + "codeSent": "Kod został właśnie wysłany na twój adres e-mail.", "submit": "Kontynuuj" } }, diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index 197b9663be..48af7d29d3 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -181,6 +181,7 @@ "description": "Введите код из письма подтверждения.", "noCodeReceived": "Не получили код?", "resendCode": "Отправить код повторно", + "codeSent": "Код отправлен на ваш email.", "submit": "Продолжить" } }, diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index d4319dc051..526f36a80b 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -181,6 +181,7 @@ "description": "输入验证邮件中的验证码。", "noCodeReceived": "没有收到验证码?", "resendCode": "重发验证码", + "codeSent": "刚刚发送了一封包含验证码的电子邮件。", "submit": "继续" } }, diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 567774b0e4..6975e6a586 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -1,4 +1,4 @@ -import { Alert } from "@/components/alert"; +import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; @@ -148,6 +148,12 @@ export default async function Page(props: { searchParams: Promise }) { )} + {id && send && ( +
+ {tError("verify.codesent")} +
+ )} + {sessionFactors ? ( Date: Wed, 21 May 2025 09:27:37 +0200 Subject: [PATCH 06/29] i18n key --- apps/login/src/app/(login)/verify/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 6975e6a586..8b04b4e152 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -150,7 +150,7 @@ export default async function Page(props: { searchParams: Promise }) { {id && send && (
- {tError("verify.codesent")} + {tError("verify.codeSent")}
)} From d59e70fe4891137d38183e8be8485b35031a6b70 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 09:45:58 +0200 Subject: [PATCH 07/29] t i18n --- apps/login/src/app/(login)/verify/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 8b04b4e152..fcbc4fd34a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -150,7 +150,7 @@ export default async function Page(props: { searchParams: Promise }) { {id && send && (
- {tError("verify.codeSent")} + {t("verify.codeSent")}
)} From b6e7dba3a6f0d575d8761682309d7ed77fae25e6 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 10:02:31 +0200 Subject: [PATCH 08/29] do not request new code --- apps/login/src/lib/verify-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 0385d62a03..a25f9ef805 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -57,7 +57,7 @@ export function checkEmailVerified( const paramsVerify = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, userId: session.factors?.user?.id as string, // verify needs user id - send: "true", // set this to true to request a new code immediately + // send: "true", // we do not request a new code again }); if (organization || session.factors?.user?.organizationId) { From 14a8e74b6910256aaf2f5f776a20d962f989c5ef Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 10:15:17 +0200 Subject: [PATCH 09/29] move userverificationcheck to server action --- apps/login/src/app/(login)/verify/page.tsx | 2 +- apps/login/src/lib/server/loginname.ts | 3 +- apps/login/src/lib/server/passkeys.ts | 6 ++-- apps/login/src/lib/server/password.ts | 2 +- apps/login/src/lib/verification-helper.ts | 34 ++++++++++++++++++++++ apps/login/src/lib/verify-helper.ts | 32 -------------------- 6 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 apps/login/src/lib/verification-helper.ts diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index fcbc4fd34a..e5b2268f14 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,7 +6,7 @@ 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 { checkUserVerification } from "@/lib/verification-helper"; import { getBrandingSettings, getUserByID, diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index bbe08dfec8..1282def867 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,7 +9,8 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkEmailVerified, checkUserVerification } from "../verify-helper"; +import { checkUserVerification } from "../verification-helper"; +import { checkEmailVerified } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 3470629f24..1a26824141 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -25,10 +25,8 @@ import { getSessionCookieByLoginName, } from "../cookies"; import { getServiceUrlFromHeaders } from "../service-url"; -import { - checkEmailVerification, - checkUserVerification, -} from "../verify-helper"; +import { checkUserVerification } from "../verification-helper"; +import { checkEmailVerification } from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 34859d419b..56158ddef1 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -29,11 +29,11 @@ import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; import { getServiceUrlFromHeaders } from "../service-url"; +import { checkUserVerification } from "../verification-helper"; import { checkEmailVerification, checkMFAFactors, checkPasswordChangeRequired, - checkUserVerification, } from "../verify-helper"; type ResetPasswordCommand = { diff --git a/apps/login/src/lib/verification-helper.ts b/apps/login/src/lib/verification-helper.ts new file mode 100644 index 0000000000..2e8565f3ac --- /dev/null +++ b/apps/login/src/lib/verification-helper.ts @@ -0,0 +1,34 @@ +"use server"; + +import crypto from "crypto"; +import { cookies } from "next/headers"; +import { getOrSetFingerprintId } from "./fingerprint"; + +export async function checkUserVerification(userId: string): Promise { + // check if a verification was done earlier + const cookiesList = await cookies(); + const userAgentId = await getOrSetFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${userId}:${userAgentId}`) + .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; +} diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index a25f9ef805..45de5df315 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -4,10 +4,7 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; import moment from "moment"; -import { cookies } from "next/headers"; -import { getOrSetFingerprintId } from "./fingerprint"; import { getUserByID } from "./zitadel"; export function checkPasswordChangeRequired( @@ -252,32 +249,3 @@ export async function checkMFAFactors( return { redirect: `/mfa/set?` + params }; } } - -export async function checkUserVerification(userId: string): Promise { - // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getOrSetFingerprintId(); - - const verificationCheck = crypto - .createHash("sha256") - .update(`${userId}:${userAgentId}`) - .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; -} From 75b682a6465a4fb5079a631728336c29e20cc11b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 11:00:18 +0200 Subject: [PATCH 10/29] only read cookie --- apps/login/src/app/(login)/verify/page.tsx | 2 +- apps/login/src/lib/server/loginname.ts | 3 +- apps/login/src/lib/server/passkeys.ts | 6 ++-- apps/login/src/lib/server/password.ts | 2 +- apps/login/src/lib/verification-helper.ts | 34 ------------------- apps/login/src/lib/verify-helper.ts | 38 ++++++++++++++++++++++ 6 files changed, 45 insertions(+), 40 deletions(-) delete mode 100644 apps/login/src/lib/verification-helper.ts diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index e5b2268f14..fcbc4fd34a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,7 +6,7 @@ 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/verification-helper"; +import { checkUserVerification } from "@/lib/verify-helper"; import { getBrandingSettings, getUserByID, diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 1282def867..bbe08dfec8 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,8 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkUserVerification } from "../verification-helper"; -import { checkEmailVerified } from "../verify-helper"; +import { checkEmailVerified, checkUserVerification } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 1a26824141..3470629f24 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -25,8 +25,10 @@ import { getSessionCookieByLoginName, } from "../cookies"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkUserVerification } from "../verification-helper"; -import { checkEmailVerification } from "../verify-helper"; +import { + checkEmailVerification, + checkUserVerification, +} from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 56158ddef1..34859d419b 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -29,11 +29,11 @@ import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkUserVerification } from "../verification-helper"; import { checkEmailVerification, checkMFAFactors, checkPasswordChangeRequired, + checkUserVerification, } from "../verify-helper"; type ResetPasswordCommand = { diff --git a/apps/login/src/lib/verification-helper.ts b/apps/login/src/lib/verification-helper.ts deleted file mode 100644 index 2e8565f3ac..0000000000 --- a/apps/login/src/lib/verification-helper.ts +++ /dev/null @@ -1,34 +0,0 @@ -"use server"; - -import crypto from "crypto"; -import { cookies } from "next/headers"; -import { getOrSetFingerprintId } from "./fingerprint"; - -export async function checkUserVerification(userId: string): Promise { - // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getOrSetFingerprintId(); - - const verificationCheck = crypto - .createHash("sha256") - .update(`${userId}:${userAgentId}`) - .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; -} diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 45de5df315..e8a18c053c 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -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 { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; import moment from "moment"; +import { cookies } from "next/headers"; +import { getFingerprintIdCookie } from "./fingerprint"; import { getUserByID } from "./zitadel"; export function checkPasswordChangeRequired( @@ -249,3 +252,38 @@ export async function checkMFAFactors( return { redirect: `/mfa/set?` + params }; } } + +export async function checkUserVerification(userId: string): Promise { + // check if a verification was done earlier + const cookiesList = await cookies(); + + // only read cookie to prevent issues on page.tsx + const userAgentId = await getFingerprintIdCookie(); + + if (!userAgentId || userAgentId.value) { + return false; + } + + const verificationCheck = crypto + .createHash("sha256") + .update(`${userId}:${userAgentId}`) + .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; +} From f6d546a8eae27fb03cc8b9cdf843f1d3c6e570de Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 11:09:35 +0200 Subject: [PATCH 11/29] autosend --- apps/login/src/lib/verify-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index e8a18c053c..7be8c46d52 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -57,7 +57,7 @@ export function checkEmailVerified( const paramsVerify = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, userId: session.factors?.user?.id as string, // verify needs user id - // send: "true", // we do not request a new code again + send: "true", // we request a new email code once the page is loaded }); if (organization || session.factors?.user?.organizationId) { From f1cb6213335276b800778be4933b4acc85694f2b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 11:42:52 +0200 Subject: [PATCH 12/29] fix user verification check --- apps/login/src/lib/server/loginname.ts | 2 +- apps/login/src/lib/server/verify.ts | 3 +-- apps/login/src/lib/verify-helper.ts | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index bbe08dfec8..83a6f90abb 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -275,7 +275,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { if (!isUserVerified) { const params = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, - // send: "true", // set this to true to request a new code immediately + send: "true", // set this to true to request a new code immediately }); if (command.requestId) { diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 07756f60ab..0ab4c5465d 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -6,7 +6,6 @@ import { getSession, getUserByID, listAuthenticationMethodTypes, - resendEmailCode, verifyEmail, verifyInviteCode, verifyTOTPRegistration, @@ -283,7 +282,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) { `${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 }) - : resendEmailCode({ + : sendEmailCode({ userId: command.userId, serviceUrl, urlTemplate: diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 7be8c46d52..dbd9b2796b 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -258,15 +258,15 @@ export async function checkUserVerification(userId: string): Promise { const cookiesList = await cookies(); // only read cookie to prevent issues on page.tsx - const userAgentId = await getFingerprintIdCookie(); + const fingerPrintCookie = await getFingerprintIdCookie(); - if (!userAgentId || userAgentId.value) { + if (!fingerPrintCookie || !fingerPrintCookie.value) { return false; } const verificationCheck = crypto .createHash("sha256") - .update(`${userId}:${userAgentId}`) + .update(`${userId}:${fingerPrintCookie.value}`) .digest("hex"); const cookieValue = await cookiesList.get("verificationCheck")?.value; From 58cb2a5fedbf557309973d583a7cb87706501499 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 17:02:22 +0200 Subject: [PATCH 13/29] rm invite page --- apps/login/src/app/(login)/invite/page.tsx | 73 ----------------- .../src/app/(login)/invite/success/page.tsx | 81 ------------------- 2 files changed, 154 deletions(-) delete mode 100644 apps/login/src/app/(login)/invite/page.tsx delete mode 100644 apps/login/src/app/(login)/invite/success/page.tsx diff --git a/apps/login/src/app/(login)/invite/page.tsx b/apps/login/src/app/(login)/invite/page.tsx deleted file mode 100644 index 11e9d732ee..0000000000 --- a/apps/login/src/app/(login)/invite/page.tsx +++ /dev/null @@ -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>; -}) { - 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 ( - -
-

{t("title")}

-

{t("description")}

- - {!loginSettings?.allowRegister ? ( - {t("notAllowed")} - ) : ( - {t("info")} - )} - - {passwordComplexitySettings && loginSettings?.allowRegister && ( - - )} -
-
- ); -} diff --git a/apps/login/src/app/(login)/invite/success/page.tsx b/apps/login/src/app/(login)/invite/success/page.tsx deleted file mode 100644 index 1b12a5b903..0000000000 --- a/apps/login/src/app/(login)/invite/success/page.tsx +++ /dev/null @@ -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>; -}) { - 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 ( - -
-

{t("success.title")}

-

{t("success.description")}

- {user && ( - - )} - {human?.email?.isVerified ? ( - {t("success.verified")} - ) : ( - {t("success.notVerifiedYet")} - )} -
- - - - -
-
-
- ); -} From b393f36b6f638ab0285ad0ed062ea3b0f679c295 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 09:15:57 +0200 Subject: [PATCH 14/29] fix "0" --- apps/login/src/components/sign-in-with-idp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index f08ebd821b..c0ab844242 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -74,7 +74,7 @@ export function SignInWithIdp({ return (
- {identityProviders?.map(renderIDPButton)} + {identityProviders.length && identityProviders?.map(renderIDPButton)} {state?.error && (
{state?.error} From eea139eca608b4f9d502eb8fb9f06308bc65867e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 10:55:53 +0200 Subject: [PATCH 15/29] 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 16/29] 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 17/29] 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 18/29] 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, From dffa94878efd7b8bf79e0c48924d56c24edf74f3 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 13:41:18 +0200 Subject: [PATCH 19/29] !! --- apps/login/src/components/sign-in-with-idp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index c0ab844242..7632a29cc1 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -74,7 +74,7 @@ export function SignInWithIdp({ return (
- {identityProviders.length && identityProviders?.map(renderIDPButton)} + {!!identityProviders.length && identityProviders?.map(renderIDPButton)} {state?.error && (
{state?.error} From e3047602404140b4434015bc575119bd6b1df4d6 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 14:09:18 +0200 Subject: [PATCH 20/29] revert mfa check on password change --- apps/login/src/lib/server/password.ts | 87 +++++++++++++++++++-------- 1 file changed, 63 insertions(+), 24 deletions(-) diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 34859d419b..3786145157 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -13,6 +13,7 @@ import { listAuthenticationMethodTypes, listUsers, passwordReset, + setPassword, setUserPassword, } from "@/lib/zitadel"; import { ConnectError, create } from "@zitadel/client"; @@ -24,7 +25,10 @@ import { } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { + AuthenticationMethodType, + SetPasswordRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; @@ -392,32 +396,67 @@ export async function checkSessionAndSetPassword({ return { error: "Could not load auth methods" }; } - const transport = async (serviceUrl: string, token: string) => { - return createServerTransport(token, { - baseUrl: serviceUrl, - }); - }; + const requiredAuthMethodsForForceMFA = [ + AuthenticationMethodType.OTP_EMAIL, + AuthenticationMethodType.OTP_SMS, + AuthenticationMethodType.TOTP, + AuthenticationMethodType.U2F, + ]; - const myUserService = async (serviceUrl: string, sessionToken: string) => { - const transportPromise = await transport(serviceUrl, sessionToken); - return createUserServiceClient(transportPromise); - }; + const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every( + (method) => !authmethods.authMethodTypes.includes(method), + ); - const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`); + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: session.factors.user.organizationId, + }); - return selfService - .setPassword( - { - userId: session.factors.user.id, - newPassword: { password, changeRequired: false }, - }, - {}, - ) - .catch((error: ConnectError) => { - console.log(error); - if (error.code === 7) { - return { error: "Session is not valid." }; + const forceMfa = !!( + loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly + ); + + // if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user + if (forceMfa && hasNoMFAMethods) { + return setPassword({ serviceUrl, payload }).catch((error) => { + // throw error if failed precondition (ex. User is not yet initialized) + if (error.code === 9 && error.message) { + return { error: "Failed precondition" }; + } else { + throw error; } - throw error; }); + } else { + const transport = async (serviceUrl: string, token: string) => { + return createServerTransport(token, { + baseUrl: serviceUrl, + }); + }; + + const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await transport(serviceUrl, sessionToken); + return createUserServiceClient(transportPromise); + }; + + const selfService = await myUserService( + serviceUrl, + `${sessionCookie.token}`, + ); + + return selfService + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error: ConnectError) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); + } } From 0af7185a903580bb5ac9b7b0f07564d6dce7714d Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 18:18:26 +0200 Subject: [PATCH 21/29] fix: use invite code whenever authmethods are zero, otherwise use email code --- .../app/(login)/authenticator/set/page.tsx | 1 + apps/login/src/app/(login)/mfa/set/page.tsx | 1 + apps/login/src/app/(login)/verify/page.tsx | 73 +++--- .../src/app/(login)/verify/success/page.tsx | 111 +++++++++ apps/login/src/components/verify-form.tsx | 5 + apps/login/src/lib/server/loginname.ts | 90 +++---- apps/login/src/lib/server/verify.ts | 231 ++++++++++-------- 7 files changed, 331 insertions(+), 181 deletions(-) create mode 100644 apps/login/src/app/(login)/verify/success/page.tsx diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 63704b87eb..5a8dfe810d 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -120,6 +120,7 @@ export default async function Page(props: { 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 }); diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index c7f2fa6599..11c44a22fa 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -134,6 +134,7 @@ export default async function Page(props: { {!(loginName || sessionId) && {tError("unknownContext")}} + {/* this happens if you register a user and open up the email verification link on a different device than the device where the registration was made. */} {!valid && {tError("sessionExpired")}} {isSessionValid(sessionWithData).valid && diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 121e250dc3..cecaa5fcf7 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -2,7 +2,7 @@ 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 { sendEmailCode } from "@/lib/server/verify"; +import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; @@ -21,11 +21,6 @@ export default async function Page(props: { searchParams: Promise }) { const _headers = await 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({ serviceUrl, @@ -41,29 +36,25 @@ export default async function Page(props: { searchParams: Promise }) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - if ("loginName" in searchParams) { - sessionFactors = await loadMostRecentSession({ - serviceUrl, - sessionParams: { - loginName, - organization, - }, - }); + async function sendEmail() { + const host = _headers.get("host"); - if (doSend && sessionFactors?.factors?.user?.id) { - await sendEmailCode({ + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + if (invite === "true") { + await sendInviteEmailCode({ serviceUrl, - userId: sessionFactors?.factors?.user?.id, + userId, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + `${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) { - if (doSend) { + } else { await sendEmailCode({ serviceUrl, userId, @@ -75,6 +66,24 @@ export default async function Page(props: { searchParams: Promise }) { throw Error("Failed to send verification email"); }); } + } + + if ("loginName" in searchParams) { + sessionFactors = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + + if (doSend && sessionFactors?.factors?.user?.id) { + await sendEmail(); + } + } else if ("userId" in searchParams && userId) { + if (doSend) { + await sendEmail(); + } const userResponse = await getUserByID({ serviceUrl, @@ -151,15 +160,19 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {/* 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) */} - + {/* always show the code form, except code is an invite code and the email is verified */} + {invite === "true" && human?.email?.isVerified ? ( + {t("success")} + ) : ( + + )}
); diff --git a/apps/login/src/app/(login)/verify/success/page.tsx b/apps/login/src/app/(login)/verify/success/page.tsx new file mode 100644 index 0000000000..aed9f79854 --- /dev/null +++ b/apps/login/src/app/(login)/verify/success/page.tsx @@ -0,0 +1,111 @@ +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 }) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "signedin" }); + + 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 ( + +
+

+ {t("title", { user: sessionFactors?.factors?.user?.displayName })} +

+

{t("description")}

+ + {sessionFactors ? ( + + ) : ( + user && ( + + ) + )} +
+
+ ); +} diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx index e09642eecf..0933f598dd 100644 --- a/apps/login/src/components/verify-form.tsx +++ b/apps/login/src/components/verify-form.tsx @@ -63,6 +63,11 @@ export function VerifyForm({ setLoading(false); }); + if (response && "error" in response && response?.error) { + setError(response.error); + return; + } + return response; } diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 83a6f90abb..1e7a1fe3de 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,7 +9,6 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkEmailVerified, checkUserVerification } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, @@ -254,62 +253,63 @@ export async function sendLoginname(command: SendLoginnameCommand) { 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) { // redirect to /verify invite if no auth method is set and email is not verified - const inviteCheck = checkEmailVerified( - session, - humanUser, - session.factors.user.organizationId, - command.requestId, - ); + // const inviteCheck = checkEmailVerified( + // session, + // humanUser, + // session.factors.user.organizationId, + // command.requestId, + // ); - if (inviteCheck?.redirect) { - return inviteCheck; - } + // if (inviteCheck?.redirect) { + // return inviteCheck; + // } - // check if user was verified recently - const isUserVerified = await checkUserVerification( - session.factors.user.id, - ); - if (!isUserVerified) { - const params = new URLSearchParams({ - loginName: session.factors?.user?.loginName as string, - send: "true", // set this to true to request a new code immediately - }); - - if (command.requestId) { - params.append("requestId", command.requestId); - } - - if (command.organization || session.factors?.user?.organizationId) { - params.append( - "organization", - command.organization ?? - (session.factors?.user?.organizationId as string), - ); - } - - return { redirect: `/verify?` + params }; - } - - const paramsAuthenticatorSetup = new URLSearchParams({ - loginName: session.factors?.user?.loginName, - userId: session.factors?.user?.id, // verify needs user id + // // check if user was verified recently + // const isUserVerified = await checkUserVerification( + // session.factors.user.id, + // ); + // if (!isUserVerified) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + send: "true", // set this to true to request a new code immediately + invite: "true", }); + if (command.requestId) { + params.append("requestId", command.requestId); + } + if (command.organization || session.factors?.user?.organizationId) { - paramsAuthenticatorSetup.append( + params.append( "organization", - command.organization ?? session.factors?.user?.organizationId, + command.organization ?? + (session.factors?.user?.organizationId as string), ); } - if (command.requestId) { - paramsAuthenticatorSetup.append("requestId", command.requestId); - } + return { redirect: `/verify?` + params }; + // } - return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup }; + // const paramsAuthenticatorSetup = new URLSearchParams({ + // loginName: session.factors?.user?.loginName, + // userId: session.factors?.user?.id, // verify needs user id + // }); + + // if (command.organization || session.factors?.user?.organizationId) { + // paramsAuthenticatorSetup.append( + // "organization", + // command.organization ?? session.factors?.user?.organizationId, + // ); + // } + + // if (command.requestId) { + // paramsAuthenticatorSetup.append("requestId", command.requestId); + // } + + // return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup }; } if (methods.authMethodTypes.length == 1) { diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 88bb0139b3..dd93cc0935 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -1,6 +1,7 @@ "use server"; import { + createInviteCode, getLoginSettings, getSession, getUserByID, @@ -93,88 +94,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { } let session: Session | undefined; - let user: User | undefined; + const userResponse = await getUserByID({ + serviceUrl, + userId: command.userId, + }); - 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({ - serviceUrl, - userId: command.userId, - }); - - if (!userResponse || !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) { + if (!userResponse || !userResponse.user) { return { error: "Could not load user" }; } - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: user.details?.resourceOwner, + const user = userResponse.user; + + const sessionCookie = await getSessionCookieByLoginName({ + loginName: + "loginName" in command ? command.loginName : user.preferredLoginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); // checked later }); + // load auth methods for user const authMethodResponse = await listAuthenticationMethodTypes({ serviceUrl, userId: user.userId, @@ -190,6 +129,36 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { authMethodResponse.authMethodTypes && 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, + }); + } else { + session = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + if (!session) { + return { error: "Could not create session" }; + } + const params = new URLSearchParams({ sessionId: session.id, }); @@ -218,44 +187,80 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { return { redirect: `/authenticator/set?${params}` }; } - // 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 no session found and user is not invited, only show success page, + // if user is invited, recreate invite flow to not depend on session - if (mfaFactorCheck?.redirect) { - return mfaFactorCheck; - } + if (!sessionCookie || !session?.factors?.user?.id) { + const verifySuccessParams = new URLSearchParams({}); - // login user if no additional steps are required - if (command.requestId && session.id) { - const nextUrl = await getNextUrl( + 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}` }; + } else { + 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( { - sessionId: session.id, - requestId: command.requestId, - organization: - command.organization ?? session.factors?.user?.organizationId, + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, }, loginSettings?.defaultRedirectUri, ); - return { redirect: nextUrl }; + return { redirect: url }; } - - const url = await getNextUrl( - { - loginName: session.factors.user.loginName, - organization: session.factors?.user?.organizationId, - }, - loginSettings?.defaultRedirectUri, - ); - - return { redirect: url }; } type resendVerifyEmailCommand = { @@ -279,6 +284,11 @@ export async function resendVerification(command: resendVerifyEmailCommand) { ? resendInviteCode({ serviceUrl, userId: command.userId, + }).catch((error) => { + if (error.code === 9) { + return { error: "User is already verified!" }; + } + return { error: "Could not resend invite" }; }) : sendEmailCode({ userId: command.userId, @@ -303,6 +313,15 @@ export async function sendEmailCode(command: sendEmailCommand) { }); } +export async function sendInviteEmailCode(command: sendEmailCommand) { + // TODO: change this to sendInvite + return createInviteCode({ + serviceUrl: command.serviceUrl, + userId: command.userId, + urlTemplate: command.urlTemplate, + }); +} + export type SendVerificationRedirectWithoutCheckCommand = { organization?: string; requestId?: string; From 8204312892f5f7230d4587769051a6242d8eff00 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 08:57:13 +0200 Subject: [PATCH 22/29] change logic --- apps/login/locales/de.json | 3 +- apps/login/locales/en.json | 3 +- apps/login/locales/es.json | 3 +- apps/login/locales/it.json | 3 +- apps/login/locales/pl.json | 3 +- apps/login/locales/ru.json | 3 +- apps/login/locales/zh.json | 3 +- apps/login/src/app/(login)/verify/page.tsx | 10 +- .../src/app/(login)/verify/success/page.tsx | 8 +- apps/login/src/lib/server/verify.ts | 97 ++++++++++--------- 10 files changed, 71 insertions(+), 65 deletions(-) diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 7471cc6ec7..8b3d4b311e 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "Keine Benutzer-ID angegeben!", - "success": "Erfolgreich verifiziert", + "successTitle": "Benutzer verifiziert", + "successDescription": "Der Benutzer wurde erfolgreich verifiziert.", "setupAuthenticator": "Authentifikator einrichten", "verify": { "title": "Benutzer verifizieren", diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 164761f43d..daaaeba108 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "No userId provided!", - "success": "The user has been verified successfully.", + "successTitle": "User verified", + "successDescription": "The user has been verified successfully.", "setupAuthenticator": "Setup authenticator", "verify": { "title": "Verify user", diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 6e5dd43820..b7dd57b4c0 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "¡No se proporcionó userId!", - "success": "¡Verificación exitosa!", + "successTitle": "Usuario verificado", + "successDescription": "El usuario ha sido verificado con éxito.", "setupAuthenticator": "Configurar autenticador", "verify": { "title": "Verificar usuario", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 1173906e82..f476da3402 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "Nessun userId fornito!", - "success": "Verifica effettuata con successo!", + "successTitle": "Utente verificato", + "successDescription": "L'utente è stato verificato con successo.", "setupAuthenticator": "Configura autenticatore", "verify": { "title": "Verifica utente", diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index ac3758227e..4dd607f3cb 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -174,7 +174,8 @@ }, "verify": { "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", "verify": { "title": "Zweryfikuj użytkownika", diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index 48af7d29d3..e8bbac212b 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "Не указан userId!", - "success": "Пользователь успешно подтверждён.", + "successTitle": "Пользователь подтверждён", + "successDescription": "Пользователь успешно подтверждён.", "setupAuthenticator": "Настроить аутентификатор", "verify": { "title": "Подтверждение пользователя", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index 526f36a80b..7bc4ecf68a 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "未提供用户 ID!", - "success": "用户验证成功。", + "successTitle": "用户已验证", + "successDescription": "用户已成功验证。", "setupAuthenticator": "设置认证器", "verify": { "title": "验证用户", diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index cecaa5fcf7..aeb6952e40 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -36,7 +36,7 @@ export default async function Page(props: { searchParams: Promise }) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - async function sendEmail() { + async function sendEmail(userId: string) { const host = _headers.get("host"); if (!host || typeof host !== "string") { @@ -51,7 +51,7 @@ export default async function Page(props: { searchParams: Promise }) { `${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); + console.error("Could not send invitation email", error); throw Error("Failed to send verification email"); }); } else { @@ -62,7 +62,7 @@ export default async function Page(props: { searchParams: Promise }) { `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { - console.error("Could not resend verification email", error); + console.error("Could not send verification email", error); throw Error("Failed to send verification email"); }); } @@ -78,11 +78,11 @@ export default async function Page(props: { searchParams: Promise }) { }); if (doSend && sessionFactors?.factors?.user?.id) { - await sendEmail(); + await sendEmail(sessionFactors.factors.user.id); } } else if ("userId" in searchParams && userId) { if (doSend) { - await sendEmail(); + await sendEmail(userId); } const userResponse = await getUserByID({ diff --git a/apps/login/src/app/(login)/verify/success/page.tsx b/apps/login/src/app/(login)/verify/success/page.tsx index aed9f79854..678687a7f6 100644 --- a/apps/login/src/app/(login)/verify/success/page.tsx +++ b/apps/login/src/app/(login)/verify/success/page.tsx @@ -33,7 +33,7 @@ async function loadSessionById( export default async function Page(props: { searchParams: Promise }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "signedin" }); + const t = await getTranslations({ locale, namespace: "verify" }); const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -84,10 +84,8 @@ export default async function Page(props: { searchParams: Promise }) { return (
-

- {t("title", { user: sessionFactors?.factors?.user?.displayName })} -

-

{t("description")}

+

{t("successTitle")}

+

{t("successDescription")}

{sessionFactors ? ( { + if (response?.session) { + return response.session; + } + }); + } + // load auth methods for user const authMethodResponse = await listAuthenticationMethodTypes({ serviceUrl, @@ -143,16 +155,6 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { checks, requestId: command.requestId, }); - } else { - session = await getSession({ - serviceUrl, - sessionId: sessionCookie.id, - sessionToken: sessionCookie.token, - }).then((response) => { - if (response?.session) { - return response.session; - } - }); } if (!session) { @@ -187,10 +189,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { return { redirect: `/authenticator/set?${params}` }; } - // if no session found and user is not invited, only show success page, + // if no session found only show success page, // if user is invited, recreate invite flow to not depend on session - - if (!sessionCookie || !session?.factors?.user?.id) { + if (!session?.factors?.user?.id) { const verifySuccessParams = new URLSearchParams({}); if (command.userId) { @@ -216,51 +217,51 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { } return { redirect: `/verify/success?${verifySuccessParams}` }; - } else { - 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, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: user.details?.resourceOwner, + }); - if (mfaFactorCheck?.redirect) { - return mfaFactorCheck; - } + // 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, + ); - // 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, - ); + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } - return { redirect: nextUrl }; - } - - const url = await getNextUrl( + // login user if no additional steps are required + if (command.requestId && session.id) { + const nextUrl = await getNextUrl( { - loginName: session.factors.user.loginName, - organization: session.factors?.user?.organizationId, + sessionId: session.id, + requestId: command.requestId, + organization: + command.organization ?? session.factors?.user?.organizationId, }, loginSettings?.defaultRedirectUri, ); - return { redirect: url }; + return { redirect: nextUrl }; } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; } type resendVerifyEmailCommand = { From 80f106dc6553042e8e535054b64deac9263030ad Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 10:17:34 +0200 Subject: [PATCH 23/29] cleanup --- apps/login/src/app/(login)/verify/page.tsx | 23 ++++++-------- apps/login/src/lib/server/loginname.ts | 36 ---------------------- 2 files changed, 9 insertions(+), 50 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index aeb6952e40..43aff0242b 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -52,7 +52,7 @@ export default async function Page(props: { searchParams: Promise }) { (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not send invitation email", error); - throw Error("Failed to send verification email"); + throw Error("Failed to send invitation email"); }); } else { await sendEmailCode({ @@ -160,19 +160,14 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {/* always show the code form, except code is an invite code and the email is verified */} - {invite === "true" && human?.email?.isVerified ? ( - {t("success")} - ) : ( - - )} +
); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 1e7a1fe3de..fa75929702 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -255,23 +255,6 @@ export async function sendLoginname(command: SendLoginnameCommand) { // always resend invite if user has no auth method set if (!methods.authMethodTypes || !methods.authMethodTypes.length) { - // redirect to /verify invite if no auth method is set and email is not verified - // const inviteCheck = checkEmailVerified( - // session, - // humanUser, - // session.factors.user.organizationId, - // command.requestId, - // ); - - // if (inviteCheck?.redirect) { - // return inviteCheck; - // } - - // // check if user was verified recently - // const isUserVerified = await checkUserVerification( - // session.factors.user.id, - // ); - // if (!isUserVerified) { const params = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, send: "true", // set this to true to request a new code immediately @@ -291,25 +274,6 @@ export async function sendLoginname(command: SendLoginnameCommand) { } return { redirect: `/verify?` + params }; - // } - - // const paramsAuthenticatorSetup = new URLSearchParams({ - // loginName: session.factors?.user?.loginName, - // userId: session.factors?.user?.id, // verify needs user id - // }); - - // if (command.organization || session.factors?.user?.organizationId) { - // paramsAuthenticatorSetup.append( - // "organization", - // command.organization ?? session.factors?.user?.organizationId, - // ); - // } - - // if (command.requestId) { - // paramsAuthenticatorSetup.append("requestId", command.requestId); - // } - - // return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup }; } if (methods.authMethodTypes.length == 1) { From fb2f1d03379e404a61f91c624a9be6c1f5789db8 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 15:54:20 +0200 Subject: [PATCH 24/29] use createinvite --- apps/login/src/lib/server/verify.ts | 7 ++++--- apps/login/src/lib/zitadel.ts | 15 --------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 5591da7290..518d6c679b 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -6,7 +6,6 @@ import { getSession, getUserByID, listAuthenticationMethodTypes, - resendInviteCode, verifyEmail, verifyInviteCode, verifyTOTPRegistration, @@ -282,9 +281,12 @@ export async function resendVerification(command: resendVerifyEmailCommand) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; return command.isInvite - ? resendInviteCode({ + ? createInviteCode({ 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!" }; @@ -315,7 +317,6 @@ export async function sendEmailCode(command: sendEmailCommand) { } export async function sendInviteEmailCode(command: sendEmailCommand) { - // TODO: change this to sendInvite return createInviteCode({ serviceUrl: command.serviceUrl, userId: command.userId, diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index d1fe83434d..a0e91a021c 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -502,21 +502,6 @@ export async function verifyInviteCode({ return userService.verifyInviteCode({ userId, verificationCode }, {}); } -export async function resendInviteCode({ - serviceUrl, - userId, -}: { - serviceUrl: string; - userId: string; -}) { - const userService: Client = await createServiceForHost( - UserService, - serviceUrl, - ); - - return userService.resendInviteCode({ userId }, {}); -} - export async function sendEmailCode({ serviceUrl, userId, From ff871aacdb44706b6e17b99f8db7b459fb93710b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 16:18:57 +0200 Subject: [PATCH 25/29] rm verify redirect button --- apps/login/src/app/(login)/mfa/set/page.tsx | 1 - .../src/components/verify-redirect-button.tsx | 102 ------------------ 2 files changed, 103 deletions(-) delete mode 100644 apps/login/src/components/verify-redirect-button.tsx diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index 11c44a22fa..c7f2fa6599 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -134,7 +134,6 @@ export default async function Page(props: { {!(loginName || sessionId) && {tError("unknownContext")}} - {/* this happens if you register a user and open up the email verification link on a different device than the device where the registration was made. */} {!valid && {tError("sessionExpired")}} {isSessionValid(sessionWithData).valid && diff --git a/apps/login/src/components/verify-redirect-button.tsx b/apps/login/src/components/verify-redirect-button.tsx deleted file mode 100644 index c968da86df..0000000000 --- a/apps/login/src/components/verify-redirect-button.tsx +++ /dev/null @@ -1,102 +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 { useRouter } from "next/navigation"; -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(""); - - const [loading, setLoading] = useState(false); - const router = useRouter(); - - async function submitAndContinue(): Promise { - setLoading(true); - - let command = { - organization, - requestId, - } as SendVerificationRedirectWithoutCheckCommand; - - if (userId) { - command = { - ...command, - userId, - } as SendVerificationRedirectWithoutCheckCommand; - } else if (loginName) { - command = { - ...command, - loginName, - } as SendVerificationRedirectWithoutCheckCommand; - } - - const response = await sendVerificationRedirectWithoutCheck(command) - .catch(() => { - setError("Could not verify"); - return; - }) - .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 ( - <> - {t("success")} - - {error && ( -
- {error} -
- )} - -
- - - {authMethods?.length === 0 && ( - - )} -
- - ); -} From 5e573c8c9c3d48d12d66ead8b6b8e7715456c65c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 16:23:32 +0200 Subject: [PATCH 26/29] cleanup unused method --- apps/login/src/lib/server/verify.ts | 167 ---------------------------- 1 file changed, 167 deletions(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 518d6c679b..aff3ebef02 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -16,7 +16,6 @@ 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 { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; @@ -323,169 +322,3 @@ export async function sendInviteEmailCode(command: sendEmailCommand) { urlTemplate: command.urlTemplate, }); } - -export type SendVerificationRedirectWithoutCheckCommand = { - organization?: string; - requestId?: string; -} & ( - | { userId: string; loginName?: never } - | { userId?: never; loginName: string } -); - -export async function sendVerificationRedirectWithoutCheck( - command: SendVerificationRedirectWithoutCheckCommand, -) { - const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); - - 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({ - 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, - userId: 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, - 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 }; -} From 5cfc458779938a42a2ecee29afa9f1f9ff490c38 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 16:26:23 +0200 Subject: [PATCH 27/29] cleanup --- apps/login/src/app/(login)/verify/page.tsx | 2 -- apps/login/src/lib/server/verify.ts | 17 +++++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 43aff0242b..7634ff063a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -45,7 +45,6 @@ export default async function Page(props: { searchParams: Promise }) { if (invite === "true") { await sendInviteEmailCode({ - serviceUrl, userId, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + @@ -56,7 +55,6 @@ export default async function Page(props: { searchParams: Promise }) { }); } else { await sendEmailCode({ - serviceUrl, userId, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index aff3ebef02..2de5cbf586 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -301,23 +301,28 @@ export async function resendVerification(command: resendVerifyEmailCommand) { }); } -type sendEmailCommand = { - serviceUrl: string; +type SendEmailCommand = { userId: 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({ - serviceUrl: command.serviceUrl, + serviceUrl, userId: command.userId, urlTemplate: command.urlTemplate, }); } -export async function sendInviteEmailCode(command: sendEmailCommand) { +export async function sendInviteEmailCode(command: SendEmailCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + return createInviteCode({ - serviceUrl: command.serviceUrl, + serviceUrl, userId: command.userId, urlTemplate: command.urlTemplate, }); From 872a9b42aeccc208f74e22914cefc3f7132079a1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 16:28:25 +0200 Subject: [PATCH 28/29] 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 2de5cbf586..cf60f739b3 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -292,7 +292,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) { } return { error: "Could not resend invite" }; }) - : sendEmailCode({ + : zitadelSendEmailCode({ userId: command.userId, serviceUrl, urlTemplate: From ce9a28d440246e5bac868077146eef25b289c081 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 28 May 2025 11:50:26 +0200 Subject: [PATCH 29/29] idp length check --- apps/login/src/app/(login)/authenticator/set/page.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 5a8dfe810d..95b89af92d 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -184,13 +184,12 @@ export default async function Page(props: { > )} - {loginSettings?.allowExternalIdp && identityProviders && ( + {loginSettings?.allowExternalIdp && !!identityProviders.length && ( <> - {identityProviders.length && ( -
-

{t("linkWithIDP")}

-
- )} +
+

{t("linkWithIDP")}

+
+