import { timestampDate } from "@zitadel/client"; import { AuthRequest } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; import { SAMLRequest } from "@zitadel/proto/zitadel/saml/v2/authorization_pb"; 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 { shouldEnforceMFA } from "./verify-helper"; import { getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, ServiceConfig } from "./zitadel"; type LoadMostRecentSessionParams = { serviceConfig: ServiceConfig; sessionParams: { loginName?: string; organization?: string; }; }; export async function loadMostRecentSession({ serviceConfig, sessionParams, }: LoadMostRecentSessionParams): Promise { const recent = await getMostRecentCookieWithLoginname({ loginName: sessionParams.loginName, organization: sessionParams.organization, }); if (!recent) { return undefined; } return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token }) .then((resp: GetSessionResponse) => resp.session) .catch(async (error) => { const { Code, ConnectError } = await import("@connectrpc/connect"); const isNotFound = error instanceof ConnectError && error.code === Code.NotFound; // The `sessions` cookie has no maxAge, so it can outlive the server-side session // and reference one the session projection no longer holds — e.g. the user logged out, an // admin/API terminated the session, or the user/org/instance was removed. In that // case getSession throws `not_found`. Treat it like the no-cookie case above and // return undefined instead of letting the error crash the caller's render. Every // call site already handles an undefined session — either with a graceful fallback // or by throwing its own explicit "no session" error. if (isNotFound) { console.warn("[Session] Could not load most recent session", error); return undefined; } throw error; }); } /** * 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({ serviceConfig, session, }: { serviceConfig: ServiceConfig; session: Session; }): Promise { // session can't be checked without user if (!session.factors?.user) { return false; } let mfaValid = true; // Check if user authenticated via different methods const validIDP = session?.factors?.intent?.verifiedAt; const validPassword = session?.factors?.password?.verifiedAt; const validPasskey = session?.factors?.webAuthN?.verifiedAt; // Get login settings to determine if MFA is actually required by policy const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors?.user?.organizationId }); // Use the existing shouldEnforceMFA function to determine if MFA is required const isMfaRequired = shouldEnforceMFA(session, loginSettings); // Always check auth methods to see if the user has MFA factors configured const authMethodTypes = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id }); const authMethods = authMethodTypes.authMethodTypes; // Filter to only MFA methods (exclude PASSWORD and PASSKEY) const mfaMethods = authMethods?.filter( (method) => method === AuthenticationMethodType.TOTP || method === AuthenticationMethodType.OTP_EMAIL || method === AuthenticationMethodType.OTP_SMS || method === AuthenticationMethodType.U2F, ); if (mfaMethods && mfaMethods.length > 0) { // User has MFA methods configured — they must be verified regardless of policy const totpValid = mfaMethods.includes(AuthenticationMethodType.TOTP) && !!session.factors.totp?.verifiedAt; const otpEmailValid = mfaMethods.includes(AuthenticationMethodType.OTP_EMAIL) && !!session.factors.otpEmail?.verifiedAt; const otpSmsValid = mfaMethods.includes(AuthenticationMethodType.OTP_SMS) && !!session.factors.otpSms?.verifiedAt; const u2fValid = mfaMethods.includes(AuthenticationMethodType.U2F) && !!session.factors.webAuthN?.verifiedAt; mfaValid = totpValid || otpEmailValid || otpSmsValid || u2fValid; } else if (isMfaRequired) { // No MFA methods configured, but MFA is forced by policy — check for any verified MFA factors const otpEmail = session.factors.otpEmail?.verifiedAt; const otpSms = session.factors.otpSms?.verifiedAt; const totp = session.factors.totp?.verifiedAt; const webAuthN = session.factors.webAuthN?.verifiedAt; mfaValid = !!(otpEmail || otpSms || totp || webAuthN); } // If user has no MFA methods and MFA is not required by policy, mfaValid remains true const stillValid = session.expirationDate ? timestampDate(session.expirationDate).getTime() > new Date().getTime() : true; if (!stillValid) { console.warn( "[Session] Session is expired", session.expirationDate ? timestampDate(session.expirationDate).toDateString() : "no expiration date", ); return false; } const validChecks = !!(validPassword || validPasskey || validIDP); if (!validChecks) { return false; } if (!mfaValid) { console.warn("[Session] MFA is required but not valid"); return false; } // Check email verification if EMAIL_VERIFICATION environment variable is enabled if (process.env.EMAIL_VERIFICATION === "true") { const userResponse = await getUserByID({ serviceConfig, 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] Email is not verified"); return false; } } return true; } export async function findValidSession({ serviceConfig, sessions, authRequest, samlRequest, organization, }: { serviceConfig: ServiceConfig; sessions: Session[]; authRequest?: AuthRequest; samlRequest?: SAMLRequest; organization?: string; }): Promise { let sessionsWithHint = sessions.filter((s) => { if (authRequest && authRequest.hintUserId) { return s.factors?.user?.id === authRequest.hintUserId; } if (authRequest && authRequest.loginHint) { return s.factors?.user?.loginName === authRequest.loginHint; } if (samlRequest) { // SAML requests don't contain user hints like OIDC (hintUserId/loginHint) // so we return all sessions for further processing return true; } return true; }); if (organization) { sessionsWithHint = sessionsWithHint.filter((s) => s.factors?.user?.organizationId === organization); } if (sessionsWithHint.length === 0) { return undefined; } // sort by change date descending sessionsWithHint.sort((a, b) => { const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0; const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0; return dateB - dateA; }); // return the first valid session according to settings for (const session of sessionsWithHint) { if (await isSessionValid({ serviceConfig, session })) { return session; } } return undefined; }