From b23c0bc6add24217a261e78270528002e5c7ddaa Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 25 Aug 2025 13:39:37 +0200 Subject: [PATCH] fix(login): add email verification check before callback (#10516) Closes https://github.com/zitadel/typescript/issues/539 This PR adds an additional email verification check before completing an auth flow, if the environment configuration `EMAIL_VERIFICATION` asks for it. # Which Problems Are Solved https://github.com/zitadel/typescript/issues/539 # How the Problems Are Solved Adds an additional check before completing an auth flow --- apps/login/src/lib/session.ts | 83 +++++++++++++++-------------------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/apps/login/src/lib/session.ts b/apps/login/src/lib/session.ts index 8c2548b8fb2..f250da64cf9 100644 --- a/apps/login/src/lib/session.ts +++ b/apps/login/src/lib/session.ts @@ -5,11 +5,7 @@ import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getMostRecentCookieWithLoginname } from "./cookies"; -import { - getLoginSettings, - getSession, - listAuthenticationMethodTypes, -} from "./zitadel"; +import { getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes } from "./zitadel"; type LoadMostRecentSessionParams = { serviceUrl: string; @@ -39,13 +35,7 @@ export async function loadMostRecentSession({ * mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.) * to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId); **/ -export async function isSessionValid({ - serviceUrl, - session, -}: { - serviceUrl: string; - session: Session; -}): Promise { +export async function isSessionValid({ serviceUrl, session }: { serviceUrl: string; session: Session }): Promise { // session can't be checked without user if (!session.factors?.user) { console.warn("Session has no user"); @@ -63,43 +53,22 @@ export async function isSessionValid({ if (authMethods && authMethods.includes(AuthenticationMethodType.TOTP)) { mfaValid = !!session.factors.totp?.verifiedAt; if (!mfaValid) { - console.warn( - "Session has no valid totpEmail factor", - session.factors.totp?.verifiedAt, - ); + console.warn("Session has no valid totpEmail factor", session.factors.totp?.verifiedAt); } - } else if ( - authMethods && - authMethods.includes(AuthenticationMethodType.OTP_EMAIL) - ) { + } else if (authMethods && authMethods.includes(AuthenticationMethodType.OTP_EMAIL)) { mfaValid = !!session.factors.otpEmail?.verifiedAt; if (!mfaValid) { - console.warn( - "Session has no valid otpEmail factor", - session.factors.otpEmail?.verifiedAt, - ); + console.warn("Session has no valid otpEmail factor", session.factors.otpEmail?.verifiedAt); } - } else if ( - authMethods && - authMethods.includes(AuthenticationMethodType.OTP_SMS) - ) { + } else if (authMethods && authMethods.includes(AuthenticationMethodType.OTP_SMS)) { mfaValid = !!session.factors.otpSms?.verifiedAt; if (!mfaValid) { - console.warn( - "Session has no valid otpSms factor", - session.factors.otpSms?.verifiedAt, - ); + console.warn("Session has no valid otpSms factor", session.factors.otpSms?.verifiedAt); } - } else if ( - authMethods && - authMethods.includes(AuthenticationMethodType.U2F) - ) { + } else if (authMethods && authMethods.includes(AuthenticationMethodType.U2F)) { mfaValid = !!session.factors.webAuthN?.verifiedAt; if (!mfaValid) { - console.warn( - "Session has no valid u2f factor", - session.factors.webAuthN?.verifiedAt, - ); + console.warn("Session has no valid u2f factor", session.factors.webAuthN?.verifiedAt); } } else { // only check settings if no auth methods are available, as this would require a setup @@ -128,22 +97,42 @@ export async function isSessionValid({ const validPasskey = session?.factors?.webAuthN?.verifiedAt; const validIDP = session?.factors?.intent?.verifiedAt; - const stillValid = session.expirationDate - ? timestampDate(session.expirationDate).getTime() > new Date().getTime() - : true; + const stillValid = session.expirationDate ? timestampDate(session.expirationDate).getTime() > new Date().getTime() : true; if (!stillValid) { console.warn( "Session is expired", - session.expirationDate - ? timestampDate(session.expirationDate).toDateString() - : "no expiration date", + session.expirationDate ? timestampDate(session.expirationDate).toDateString() : "no expiration date", ); + return false; } const validChecks = !!(validPassword || validPasskey || validIDP); - return stillValid && validChecks && mfaValid; + if (!validChecks) { + return false; + } + + if (!mfaValid) { + return false; + } + + // Check email verification if EMAIL_VERIFICATION environment variable is enabled + if (process.env.EMAIL_VERIFICATION === "true") { + const userResponse = await getUserByID({ + serviceUrl, + userId: session.factors.user.id, + }); + + const humanUser = userResponse?.user?.type.case === "human" ? userResponse?.user.type.value : undefined; + + if (humanUser && !humanUser.email?.isVerified) { + console.warn("Session invalid: Email not verified and EMAIL_VERIFICATION is enabled", session.factors.user.id); + return false; + } + } + + return true; } export async function findValidSession({