mirror of
https://github.com/zitadel/zitadel.git
synced 2025-11-01 00:46:23 +00:00
fix(login): Safari Cookie Issues in Development Mode (#10594)
Safari was not creating session cookies during local development, causing authentication failures. This was due to nextjs default setting of SameSite cookie property. We explicitly set "strict" for session cookies now. Closes #10473 # Which Problems Are Solved Authentication Issues with Safari in local development # How the Problems Are Solved - Cleaner API: Replaced confusing sameSite boolean/string parameters with iFrameEnabled boolean - Better logic flow: iFrameEnabled: true → sameSite: "none" (for iframe embedding) Production → sameSite: "strict" (maximum security)
This commit is contained in:
@@ -20,18 +20,26 @@ export type Cookie = {
|
||||
|
||||
type SessionCookie<T> = Cookie & T;
|
||||
|
||||
async function setSessionHttpOnlyCookie<T>(
|
||||
sessions: SessionCookie<T>[],
|
||||
sameSite: boolean | "lax" | "strict" | "none" = true,
|
||||
) {
|
||||
async function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[], 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<T>({
|
||||
session,
|
||||
cleanup,
|
||||
sameSite,
|
||||
iFrameEnabled,
|
||||
}: {
|
||||
session: SessionCookie<T>;
|
||||
cleanup?: boolean;
|
||||
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
|
||||
iFrameEnabled?: boolean;
|
||||
}): Promise<any> {
|
||||
const cookiesList = await cookies();
|
||||
const stringifiedCookie = cookiesList.get("sessions");
|
||||
|
||||
let currentSessions: SessionCookie<T>[] = stringifiedCookie?.value
|
||||
? JSON.parse(stringifiedCookie?.value)
|
||||
: [];
|
||||
let currentSessions: SessionCookie<T>[] = 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<T>({
|
||||
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<T>({
|
||||
id,
|
||||
session,
|
||||
cleanup,
|
||||
sameSite,
|
||||
iFrameEnabled,
|
||||
}: {
|
||||
id: string;
|
||||
session: SessionCookie<T>;
|
||||
cleanup?: boolean;
|
||||
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
|
||||
iFrameEnabled?: boolean;
|
||||
}): Promise<any> {
|
||||
const cookiesList = await cookies();
|
||||
const stringifiedCookie = cookiesList.get("sessions");
|
||||
|
||||
const sessions: SessionCookie<T>[] = stringifiedCookie?.value
|
||||
? JSON.parse(stringifiedCookie?.value)
|
||||
: [session];
|
||||
const sessions: SessionCookie<T>[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : [session];
|
||||
|
||||
const foundIndex = sessions.findIndex((session) => session.id === id);
|
||||
|
||||
@@ -120,13 +120,11 @@ export async function updateSessionCookie<T>({
|
||||
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<T>: session id now found";
|
||||
@@ -136,30 +134,26 @@ export async function updateSessionCookie<T>({
|
||||
export async function removeSessionFromCookie<T>({
|
||||
session,
|
||||
cleanup,
|
||||
sameSite,
|
||||
iFrameEnabled,
|
||||
}: {
|
||||
session: SessionCookie<T>;
|
||||
cleanup?: boolean;
|
||||
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
|
||||
iFrameEnabled?: boolean;
|
||||
}) {
|
||||
const cookiesList = await cookies();
|
||||
const stringifiedCookie = cookiesList.get("sessions");
|
||||
|
||||
const sessions: SessionCookie<T>[] = stringifiedCookie?.value
|
||||
? JSON.parse(stringifiedCookie?.value)
|
||||
: [session];
|
||||
const sessions: SessionCookie<T>[] = 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<T>({
|
||||
const sessions: SessionCookie<T>[] = 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<T>({
|
||||
if (stringifiedCookie?.value) {
|
||||
const sessions: SessionCookie<T>[] = 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<T>({
|
||||
* @param cleanup when true, removes all expired sessions, default true
|
||||
* @returns Session Cookies
|
||||
*/
|
||||
export async function getAllSessionCookieIds<T>(
|
||||
cleanup: boolean = false,
|
||||
): Promise<string[]> {
|
||||
export async function getAllSessionCookieIds<T>(cleanup: boolean = false): Promise<string[]> {
|
||||
const cookiesList = await cookies();
|
||||
const stringifiedCookie = cookiesList.get("sessions");
|
||||
|
||||
@@ -253,9 +241,7 @@ export async function getAllSessionCookieIds<T>(
|
||||
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<T>(
|
||||
* @param cleanup when true, removes all expired sessions, default true
|
||||
* @returns Session Cookies
|
||||
*/
|
||||
export async function getAllSessions<T>(
|
||||
cleanup: boolean = false,
|
||||
): Promise<SessionCookie<T>[]> {
|
||||
export async function getAllSessions<T>(cleanup: boolean = false): Promise<SessionCookie<T>[]> {
|
||||
const cookiesList = await cookies();
|
||||
const stringifiedCookie = cookiesList.get("sessions");
|
||||
|
||||
@@ -283,9 +267,7 @@ export async function getAllSessions<T>(
|
||||
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;
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user