From 575831f2528306ca4f6d12bb92aec5d678600823 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 19 May 2025 14:36:26 +0200 Subject: [PATCH 01/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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;