diff --git a/apps/login/src/lib/cookies.ts b/apps/login/src/lib/cookies.ts index 71008de24ed..212414a63c4 100644 --- a/apps/login/src/lib/cookies.ts +++ b/apps/login/src/lib/cookies.ts @@ -20,18 +20,26 @@ export type Cookie = { type SessionCookie = Cookie & T; -async function setSessionHttpOnlyCookie( - sessions: SessionCookie[], - sameSite: boolean | "lax" | "strict" | "none" = true, -) { +async function setSessionHttpOnlyCookie(sessions: SessionCookie[], iFrameEnabled: boolean = false) { const cookiesList = await cookies(); + // Use "none" for iframe compatibility, otherwise "strict" as default + let resolvedSameSite: "lax" | "strict" | "none"; + + if (iFrameEnabled) { + // When embedded in iframe, must use "none" with secure flag + resolvedSameSite = "none"; + } else { + // Production and other environments: use strict for better security + resolvedSameSite = "strict"; + } + return cookiesList.set({ name: "sessions", value: JSON.stringify(sessions), httpOnly: true, path: "/", - sameSite: process.env.NODE_ENV === "production" ? sameSite : "lax", + sameSite: resolvedSameSite, secure: process.env.NODE_ENV === "production", }); } @@ -50,22 +58,18 @@ export async function setLanguageCookie(language: string) { export async function addSessionToCookie({ session, cleanup, - sameSite, + iFrameEnabled, }: { session: SessionCookie; cleanup?: boolean; - sameSite?: boolean | "lax" | "strict" | "none" | undefined; + iFrameEnabled?: boolean; }): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); - let currentSessions: SessionCookie[] = stringifiedCookie?.value - ? JSON.parse(stringifiedCookie?.value) - : []; + let currentSessions: SessionCookie[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : []; - const index = currentSessions.findIndex( - (s) => s.loginName === session.loginName, - ); + const index = currentSessions.findIndex((s) => s.loginName === session.loginName); if (index > -1) { currentSessions[index] = session; @@ -85,13 +89,11 @@ export async function addSessionToCookie({ if (cleanup) { const now = new Date(); const filteredSessions = currentSessions.filter((session) => - session.expirationTs - ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now - : true, + session.expirationTs ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ); - return setSessionHttpOnlyCookie(filteredSessions, sameSite); + return setSessionHttpOnlyCookie(filteredSessions, iFrameEnabled); } else { - return setSessionHttpOnlyCookie(currentSessions, sameSite); + return setSessionHttpOnlyCookie(currentSessions, iFrameEnabled); } } @@ -99,19 +101,17 @@ export async function updateSessionCookie({ id, session, cleanup, - sameSite, + iFrameEnabled, }: { id: string; session: SessionCookie; cleanup?: boolean; - sameSite?: boolean | "lax" | "strict" | "none" | undefined; + iFrameEnabled?: boolean; }): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); - const sessions: SessionCookie[] = stringifiedCookie?.value - ? JSON.parse(stringifiedCookie?.value) - : [session]; + const sessions: SessionCookie[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : [session]; const foundIndex = sessions.findIndex((session) => session.id === id); @@ -120,13 +120,11 @@ export async function updateSessionCookie({ if (cleanup) { const now = new Date(); const filteredSessions = sessions.filter((session) => - session.expirationTs - ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now - : true, + session.expirationTs ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ); - return setSessionHttpOnlyCookie(filteredSessions, sameSite); + return setSessionHttpOnlyCookie(filteredSessions, iFrameEnabled); } else { - return setSessionHttpOnlyCookie(sessions, sameSite); + return setSessionHttpOnlyCookie(sessions, iFrameEnabled); } } else { throw "updateSessionCookie: session id now found"; @@ -136,30 +134,26 @@ export async function updateSessionCookie({ export async function removeSessionFromCookie({ session, cleanup, - sameSite, + iFrameEnabled, }: { session: SessionCookie; cleanup?: boolean; - sameSite?: boolean | "lax" | "strict" | "none" | undefined; + iFrameEnabled?: boolean; }) { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); - const sessions: SessionCookie[] = stringifiedCookie?.value - ? JSON.parse(stringifiedCookie?.value) - : [session]; + const sessions: SessionCookie[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : [session]; const reducedSessions = sessions.filter((s) => s.id !== session.id); if (cleanup) { const now = new Date(); const filteredSessions = reducedSessions.filter((session) => - session.expirationTs - ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now - : true, + session.expirationTs ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ); - return setSessionHttpOnlyCookie(filteredSessions, sameSite); + return setSessionHttpOnlyCookie(filteredSessions, iFrameEnabled); } else { - return setSessionHttpOnlyCookie(reducedSessions, sameSite); + return setSessionHttpOnlyCookie(reducedSessions, iFrameEnabled); } } @@ -194,9 +188,7 @@ export async function getSessionCookieById({ const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); const found = sessions.find((s) => - organization - ? s.organization === organization && s.id === sessionId - : s.id === sessionId, + organization ? s.organization === organization && s.id === sessionId : s.id === sessionId, ); if (found) { return found; @@ -221,9 +213,7 @@ export async function getSessionCookieByLoginName({ if (stringifiedCookie?.value) { const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); const found = sessions.find((s) => - organization - ? s.organization === organization && s.loginName === loginName - : s.loginName === loginName, + organization ? s.organization === organization && s.loginName === loginName : s.loginName === loginName, ); if (found) { return found; @@ -240,9 +230,7 @@ export async function getSessionCookieByLoginName({ * @param cleanup when true, removes all expired sessions, default true * @returns Session Cookies */ -export async function getAllSessionCookieIds( - cleanup: boolean = false, -): Promise { +export async function getAllSessionCookieIds(cleanup: boolean = false): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); @@ -253,9 +241,7 @@ export async function getAllSessionCookieIds( const now = new Date(); return sessions .filter((session) => - session.expirationTs - ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now - : true, + session.expirationTs ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ) .map((session) => session.id); } else { @@ -271,9 +257,7 @@ export async function getAllSessionCookieIds( * @param cleanup when true, removes all expired sessions, default true * @returns Session Cookies */ -export async function getAllSessions( - cleanup: boolean = false, -): Promise[]> { +export async function getAllSessions(cleanup: boolean = false): Promise[]> { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); @@ -283,9 +267,7 @@ export async function getAllSessions( if (cleanup) { const now = new Date(); return sessions.filter((session) => - session.expirationTs - ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now - : true, + session.expirationTs ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ); } else { return sessions; diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 7f87f497316..551d95db5ed 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -9,15 +9,8 @@ import { setSession, } from "@/lib/zitadel"; import { ConnectError, Duration, timestampMs } from "@zitadel/client"; -import { - CredentialsCheckError, - CredentialsCheckErrorSchema, - ErrorDetail, -} from "@zitadel/proto/zitadel/message_pb"; -import { - Challenges, - RequestChallenges, -} from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { CredentialsCheckError, CredentialsCheckErrorSchema, ErrorDetail } from "@zitadel/proto/zitadel/message_pb"; +import { Challenges, RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { headers } from "next/headers"; @@ -82,15 +75,9 @@ export async function createSessionAndUpdateCookie(command: { const sessionCookie: CustomCookieData = { id: createdSession.sessionId, token: createdSession.sessionToken, - creationTs: response.session.creationDate - ? `${timestampMs(response.session.creationDate)}` - : "", - expirationTs: response.session.expirationDate - ? `${timestampMs(response.session.expirationDate)}` - : "", - changeTs: response.session.changeDate - ? `${timestampMs(response.session.changeDate)}` - : "", + creationTs: response.session.creationDate ? `${timestampMs(response.session.creationDate)}` : "", + expirationTs: response.session.expirationDate ? `${timestampMs(response.session.expirationDate)}` : "", + changeTs: response.session.changeDate ? `${timestampMs(response.session.changeDate)}` : "", loginName: response.session.factors.user.loginName ?? "", }; @@ -99,16 +86,13 @@ export async function createSessionAndUpdateCookie(command: { } if (response.session.factors.user.organizationId) { - sessionCookie.organization = - response.session.factors.user.organizationId; + sessionCookie.organization = response.session.factors.user.organizationId; } const securitySettings = await getSecuritySettings({ serviceUrl }); - const sameSite = securitySettings?.embeddedIframe?.enabled - ? "none" - : true; + const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled; - await addSessionToCookie({ session: sessionCookie, sameSite }); + await addSessionToCookie({ session: sessionCookie, iFrameEnabled }); return response.session as Session; } else { @@ -140,9 +124,7 @@ export async function createSessionForIdpAndUpdateCookie({ let sessionLifetime = lifetime; if (!sessionLifetime) { - console.warn( - "No IDP session lifetime provided, using default of 24 hours.", - ); + console.warn("No IDP session lifetime provided, using default of 24 hours."); sessionLifetime = { seconds: BigInt(24 * 60 * 60), // 24 hours @@ -183,12 +165,8 @@ export async function createSessionForIdpAndUpdateCookie({ const sessionCookie: CustomCookieData = { id: createdSession.sessionId, token: createdSession.sessionToken, - creationTs: session.creationDate - ? `${timestampMs(session.creationDate)}` - : "", - expirationTs: session.expirationDate - ? `${timestampMs(session.expirationDate)}` - : "", + creationTs: session.creationDate ? `${timestampMs(session.creationDate)}` : "", + expirationTs: session.expirationDate ? `${timestampMs(session.expirationDate)}` : "", changeTs: session.changeDate ? `${timestampMs(session.changeDate)}` : "", loginName: session.factors.user.loginName ?? "", organization: session.factors.user.organizationId ?? "", @@ -203,9 +181,9 @@ export async function createSessionForIdpAndUpdateCookie({ } const securitySettings = await getSecuritySettings({ serviceUrl }); - const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true; + const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled; - return addSessionToCookie({ session: sessionCookie, sameSite }).then(() => { + return addSessionToCookie({ session: sessionCookie, iFrameEnabled }).then(() => { return session as Session; }); } @@ -240,9 +218,7 @@ export async function setSessionAndUpdateCookie(command: { creationTs: command.recentCookie.creationTs, expirationTs: command.recentCookie.expirationTs, // just overwrite the changeDate with the new one - changeTs: updatedSession.details?.changeDate - ? `${timestampMs(updatedSession.details.changeDate)}` - : "", + changeTs: updatedSession.details?.changeDate ? `${timestampMs(updatedSession.details.changeDate)}` : "", loginName: command.recentCookie.loginName, organization: command.recentCookie.organization, }; @@ -256,10 +232,7 @@ export async function setSessionAndUpdateCookie(command: { sessionId: sessionCookie.id, sessionToken: sessionCookie.token, }).then(async (response) => { - if ( - !response?.session || - !response.session.factors?.user?.loginName - ) { + if (!response?.session || !response.session.factors?.user?.loginName) { throw "could not get session or session does not have loginName"; } @@ -270,9 +243,7 @@ export async function setSessionAndUpdateCookie(command: { creationTs: sessionCookie.creationTs, expirationTs: sessionCookie.expirationTs, // just overwrite the changeDate with the new one - changeTs: updatedSession.details?.changeDate - ? `${timestampMs(updatedSession.details.changeDate)}` - : "", + changeTs: updatedSession.details?.changeDate ? `${timestampMs(updatedSession.details.changeDate)}` : "", loginName: session.factors?.user?.loginName ?? "", organization: session.factors?.user?.organizationId ?? "", }; @@ -282,14 +253,12 @@ export async function setSessionAndUpdateCookie(command: { } const securitySettings = await getSecuritySettings({ serviceUrl }); - const sameSite = securitySettings?.embeddedIframe?.enabled - ? "none" - : true; + const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled; return updateSessionCookie({ id: sessionCookie.id, session: newCookie, - sameSite, + iFrameEnabled, }).then(() => { return { challenges: updatedSession.challenges, ...session }; }); diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 957c89ad814..7f8e9e46e9f 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -69,10 +69,7 @@ export async function skipMFAAndContinueWithNextUrl({ } } -export async function continueWithSession({ - requestId, - ...session -}: Session & { requestId?: string }) { +export async function continueWithSession({ requestId, ...session }: Session & { requestId?: string }) { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -116,8 +113,7 @@ export type UpdateSessionCommand = { }; export async function updateSession(options: UpdateSessionCommand) { - let { loginName, sessionId, organization, checks, requestId, challenges } = - options; + let { loginName, sessionId, organization, checks, requestId, challenges } = options; const recentSession = sessionId ? await getSessionCookieById({ sessionId }) : loginName @@ -138,12 +134,7 @@ export async function updateSession(options: UpdateSessionCommand) { return { error: "Could not get host" }; } - if ( - host && - challenges && - challenges.webAuthN && - !challenges.webAuthN.domain - ) { + if (host && challenges && challenges.webAuthN && !challenges.webAuthN.domain) { const [hostname] = host.split(":"); challenges.webAuthN.domain = hostname; @@ -219,11 +210,11 @@ export async function clearSession(options: ClearSessionOptions) { }); const securitySettings = await getSecuritySettings({ serviceUrl }); - const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true; + const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled; if (!deleteResponse) { throw new Error("Could not delete session"); } - return removeSessionFromCookie({ session: sessionCookie, sameSite }); + return removeSessionFromCookie({ session: sessionCookie, iFrameEnabled }); }