Files
zitadel/apps/login/src/lib/cookies.test.ts
T
26f8238499 fix(login): correctly redirect to /password when ignoreUnknownUsername is set (#11130)
Closes #11006 

# Which Problems Are Solved

This PR addresses an issue where the ignoreUnknownUsernames setting was
not being respected in certain scenarios during the login flow.
Specifically:

- When a user was found but rejected due to login settings (e.g.,
disableLoginWithEmail), the system would return a "User not found" error
instead of redirecting to the password page as dictated by
ignoreUnknownUsernames.
- When a user was not found, and the flow fell through the registration
checks (e.g., IDP redirect failed), it would also return "User not
found" without checking ignoreUnknownUsernames.

# How the Problems Are Solved

- Introduced a helper function `handleUserNotFound` in 
`apps/login/src/lib/server/loginname.ts` to centralize the logic for
checking ignoreUnknownUsernames and redirecting to the password page.
- Updated the user validation logic to call handleUserNotFound when a
user is found but rejected by policy.
- Updated the registration fallback logic to call handleUserNotFound
instead of directly returning an error.
- Added new test cases in `apps/login/src/lib/server/loginname.test.ts`
to verify the fix and ensure no regressions.

# Additional Changes

- Fixed inconsistency in translation files

---------

Co-authored-by: Livio Spring <[email protected]>
2026-01-14 08:24:48 +00:00

911 lines
26 KiB
TypeScript

import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import {
Cookie,
addSessionToCookie,
updateSessionCookie,
removeSessionFromCookie,
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
getAllSessionCookieIds,
getAllSessions,
getMostRecentCookieWithLoginname,
setLanguageCookie,
} from "./cookies";
// Mock dependencies
vi.mock("next/headers", () => ({
cookies: vi.fn(),
}));
vi.mock("@zitadel/client", () => ({
timestampDate: vi.fn((ts: any) => new Date(Number(ts.seconds) * 1000)),
timestampFromMs: vi.fn((ms: number) => ({ seconds: BigInt(Math.floor(ms / 1000)) }) as any),
}));
import { cookies } from "next/headers";
import { timestampDate, timestampFromMs } from "@zitadel/client";
describe("cookies", () => {
let mockCookies: any;
beforeEach(() => {
vi.clearAllMocks();
mockCookies = {
get: vi.fn(),
set: vi.fn(),
};
vi.mocked(cookies).mockResolvedValue(mockCookies);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("setLanguageCookie", () => {
it("should set language cookie with correct parameters", async () => {
await setLanguageCookie("en");
expect(mockCookies.set).toHaveBeenCalledWith({
name: "NEXT_LOCALE",
value: "en",
httpOnly: true,
path: "/",
});
});
});
describe("addSessionToCookie", () => {
const mockSession: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
organization: "org-1",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
it("should add new session to empty cookie", async () => {
mockCookies.get.mockReturnValue(undefined);
await addSessionToCookie({ session: mockSession });
expect(mockCookies.set).toHaveBeenCalledWith(
expect.objectContaining({
name: "sessions",
value: JSON.stringify([mockSession]),
}),
);
});
it("should prepend new session to existing sessions", async () => {
const existingSession: Cookie = {
id: "session-0",
token: "token-0",
loginName: "[email protected]",
creationTs: "1600000000000",
expirationTs: "1700000000000",
changeTs: "1600000000000",
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([existingSession]),
});
await addSessionToCookie({ session: mockSession });
const expectedSessions = [mockSession, existingSession];
expect(mockCookies.set).toHaveBeenCalledWith(
expect.objectContaining({
value: JSON.stringify(expectedSessions),
}),
);
});
it("should update existing session with same loginName", async () => {
const existingSession: Cookie = {
id: "session-old",
token: "token-old",
loginName: "[email protected]",
creationTs: "1600000000000",
expirationTs: "1700000000000",
changeTs: "1600000000000",
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([existingSession]),
});
const updatedSession: Cookie = {
...mockSession,
id: "session-new",
token: "token-new",
};
await addSessionToCookie({ session: updatedSession });
const setCall = mockCookies.set.mock.calls[0][0];
const sessions = JSON.parse(setCall.value);
expect(sessions).toHaveLength(1);
expect(sessions[0].id).toBe("session-new");
expect(sessions[0].token).toBe("token-new");
});
it("should handle cookie overflow by replacing oldest session", async () => {
// Create many sessions to exceed MAX_COOKIE_SIZE (2048)
const manySessions: Cookie[] = Array.from({ length: 10 }, (_, i) => ({
id: `session-${i}`,
token: `token-${i}-very-long-token-to-increase-size-padding-padding-padding`,
loginName: `user${i}@example.com`,
creationTs: `${1600000000000 + i * 1000}`,
expirationTs: `${1700000000000 + i * 1000}`,
changeTs: `${1600000000000 + i * 1000}`,
}));
mockCookies.get.mockReturnValue({
value: JSON.stringify(manySessions),
});
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await addSessionToCookie({ session: mockSession });
expect(consoleSpy).toHaveBeenCalledWith("WARNING COOKIE OVERFLOW");
const setCall = mockCookies.set.mock.calls[0][0];
const sessions = JSON.parse(setCall.value);
// Should have new session and all but the first old session
expect(sessions[0]).toEqual(mockSession);
expect(sessions).not.toContain(manySessions[0]);
consoleSpy.mockRestore();
});
it("should cleanup expired sessions when cleanup is true", async () => {
const now = Date.now();
const expiredSession: Cookie = {
id: "session-expired",
token: "token-expired",
loginName: "[email protected]",
creationTs: `${now - 10000000}`,
expirationTs: `${now - 1000}`, // Expired
changeTs: `${now - 10000000}`,
};
const validSession: Cookie = {
id: "session-valid",
token: "token-valid",
loginName: "[email protected]",
creationTs: `${now}`,
expirationTs: `${now + 10000000}`, // Not expired
changeTs: `${now}`,
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([expiredSession, validSession]),
});
vi.mocked(timestampDate).mockImplementation((ts: any) => new Date(Number(ts.seconds) * 1000));
vi.mocked(timestampFromMs).mockImplementation((ms: number) => ({ seconds: BigInt(Math.floor(ms / 1000)) }) as any);
await addSessionToCookie({ session: mockSession, cleanup: true });
const setCall = mockCookies.set.mock.calls[0][0];
const sessions = JSON.parse(setCall.value);
// Should not include expired session
const hasExpired = sessions.some((s: Cookie) => s.id === "session-expired");
expect(hasExpired).toBe(false);
});
it("should set sameSite to 'none' when iFrameEnabled is true", async () => {
mockCookies.get.mockReturnValue(undefined);
await addSessionToCookie({ session: mockSession, iFrameEnabled: true });
expect(mockCookies.set).toHaveBeenCalledWith(
expect.objectContaining({
sameSite: "none",
}),
);
});
it("should set sameSite to 'lax' when iFrameEnabled is false or undefined", async () => {
mockCookies.get.mockReturnValue(undefined);
await addSessionToCookie({ session: mockSession, iFrameEnabled: false });
expect(mockCookies.set).toHaveBeenCalledWith(
expect.objectContaining({
sameSite: "lax",
}),
);
mockCookies.set.mockClear();
await addSessionToCookie({ session: mockSession });
expect(mockCookies.set).toHaveBeenCalledWith(
expect.objectContaining({
sameSite: "lax",
}),
);
});
});
describe("updateSessionCookie", () => {
const mockSession: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
it("should update existing session by id", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([mockSession]),
});
const updatedSession: Cookie = {
...mockSession,
token: "new-token",
changeTs: "1700000001000",
};
await updateSessionCookie({
id: "session-1",
session: updatedSession,
});
const setCall = mockCookies.set.mock.calls[0][0];
const sessions = JSON.parse(setCall.value);
expect(sessions[0].token).toBe("new-token");
expect(sessions[0].changeTs).toBe("1700000001000");
});
it("should throw error if session id not found", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([mockSession]),
});
await expect(
updateSessionCookie({
id: "non-existent-id",
session: mockSession,
}),
).rejects.toThrow("updateSessionCookie<T>: session id not found");
});
it("should cleanup expired sessions when cleanup is true", async () => {
const now = Date.now();
const expiredSession: Cookie = {
id: "session-expired",
token: "token-expired",
loginName: "[email protected]",
creationTs: `${now - 10000000}`,
expirationTs: `${now - 1000}`,
changeTs: `${now - 10000000}`,
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([mockSession, expiredSession]),
});
vi.mocked(timestampDate).mockImplementation((ts: any) => new Date(Number(ts.seconds) * 1000));
vi.mocked(timestampFromMs).mockImplementation((ms: number) => ({ seconds: BigInt(Math.floor(ms / 1000)) }) as any);
await updateSessionCookie({
id: "session-1",
session: { ...mockSession, token: "updated" },
cleanup: true,
});
const setCall = mockCookies.set.mock.calls[0][0];
const sessions = JSON.parse(setCall.value);
const hasExpired = sessions.some((s: Cookie) => s.id === "session-expired");
expect(hasExpired).toBe(false);
});
it("should respect iFrameEnabled parameter", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([mockSession]),
});
await updateSessionCookie({
id: "session-1",
session: mockSession,
iFrameEnabled: true,
});
expect(mockCookies.set).toHaveBeenCalledWith(
expect.objectContaining({
sameSite: "none",
}),
);
});
});
describe("removeSessionFromCookie", () => {
const session1: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
const session2: Cookie = {
id: "session-2",
token: "token-2",
loginName: "[email protected]",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000001000",
};
it("should remove session by id", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
await removeSessionFromCookie({ session: session1 });
const setCall = mockCookies.set.mock.calls[0][0];
const sessions = JSON.parse(setCall.value);
expect(sessions).toHaveLength(1);
expect(sessions[0].id).toBe("session-2");
});
it("should handle removing non-existent session gracefully", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1]),
});
const nonExistentSession: Cookie = {
...session2,
id: "non-existent",
};
await removeSessionFromCookie({ session: nonExistentSession });
const setCall = mockCookies.set.mock.calls[0][0];
const sessions = JSON.parse(setCall.value);
expect(sessions).toHaveLength(1);
expect(sessions[0].id).toBe("session-1");
});
it("should cleanup expired sessions when cleanup is true", async () => {
const now = Date.now();
const expiredSession: Cookie = {
id: "session-expired",
token: "token-expired",
loginName: "[email protected]",
creationTs: `${now - 10000000}`,
expirationTs: `${now - 1000}`,
changeTs: `${now - 10000000}`,
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2, expiredSession]),
});
vi.mocked(timestampDate).mockImplementation((ts: any) => new Date(Number(ts.seconds) * 1000));
vi.mocked(timestampFromMs).mockImplementation((ms: number) => ({ seconds: BigInt(Math.floor(ms / 1000)) }) as any);
await removeSessionFromCookie({ session: session1, cleanup: true });
const setCall = mockCookies.set.mock.calls[0][0];
const sessions = JSON.parse(setCall.value);
expect(sessions).toHaveLength(1);
expect(sessions[0].id).toBe("session-2");
});
it("should respect iFrameEnabled parameter", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
await removeSessionFromCookie({
session: session1,
iFrameEnabled: true,
});
expect(mockCookies.set).toHaveBeenCalledWith(
expect.objectContaining({
sameSite: "none",
}),
);
});
});
describe("getMostRecentSessionCookie", () => {
it("should return session with most recent changeTs", async () => {
const session1: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
const session2: Cookie = {
id: "session-2",
token: "token-2",
loginName: "[email protected]",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000005000", // Most recent
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
const result = await getMostRecentSessionCookie();
expect(result).toBeDefined();
expect(result?.id).toBe("session-2");
});
it("should return undefined when no session cookie exists", async () => {
mockCookies.get.mockReturnValue(undefined);
await expect(getMostRecentSessionCookie()).resolves.toBeUndefined();
});
it("should handle single session", async () => {
const session: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([session]),
});
const result = await getMostRecentSessionCookie();
expect(result).toBeDefined();
expect(result?.id).toBe("session-1");
});
});
describe("getSessionCookieById", () => {
const session1: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
organization: "org-1",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
const session2: Cookie = {
id: "session-2",
token: "token-2",
loginName: "[email protected]",
organization: "org-2",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000001000",
};
it("should find session by id", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
const result = await getSessionCookieById({ sessionId: "session-1" });
expect(result).toBeDefined();
expect(result?.id).toBe("session-1");
expect(result?.loginName).toBe("[email protected]");
});
it("should filter by organization when provided", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
const result = await getSessionCookieById({
sessionId: "session-2",
organization: "org-2",
});
expect(result).toBeDefined();
expect(result?.id).toBe("session-2");
expect(result?.organization).toBe("org-2");
});
it("should return undefined if session not found", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1]),
});
await expect(getSessionCookieById({ sessionId: "non-existent" })).resolves.toBeUndefined();
});
it("should return undefined if organization doesn't match", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1]),
});
await expect(
getSessionCookieById({
sessionId: "session-1",
organization: "wrong-org",
}),
).resolves.toBeUndefined();
});
it("should return undefined when no session cookie exists", async () => {
mockCookies.get.mockReturnValue(undefined);
await expect(getSessionCookieById({ sessionId: "session-1" })).resolves.toBeUndefined();
});
});
describe("getSessionCookieByLoginName", () => {
const session1: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
organization: "org-1",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
const session2: Cookie = {
id: "session-2",
token: "token-2",
loginName: "[email protected]",
organization: "org-2",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000001000",
};
it("should find session by loginName", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
const result = await getSessionCookieByLoginName({
loginName: "[email protected]",
});
expect(result).toBeDefined();
expect(result?.id).toBe("session-1");
expect(result?.loginName).toBe("[email protected]");
});
it("should filter by organization when provided", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
const result = await getSessionCookieByLoginName({
loginName: "[email protected]",
organization: "org-2",
});
expect(result).toBeDefined();
expect(result?.id).toBe("session-2");
expect(result?.organization).toBe("org-2");
});
it("should return undefined if session not found", async () => {
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1]),
});
await expect(getSessionCookieByLoginName({ loginName: "[email protected]" })).resolves.toBeUndefined();
});
it("should return undefined when no session cookie exists", async () => {
mockCookies.get.mockReturnValue(undefined);
await expect(getSessionCookieByLoginName({ loginName: "[email protected]" })).resolves.toBeUndefined();
});
});
describe("getAllSessionCookieIds", () => {
it("should return all session IDs", async () => {
const sessions: Cookie[] = [
{
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
},
{
id: "session-2",
token: "token-2",
loginName: "[email protected]",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000001000",
},
];
mockCookies.get.mockReturnValue({
value: JSON.stringify(sessions),
});
const result = await getAllSessionCookieIds();
expect(result).toEqual(["session-1", "session-2"]);
});
it("should filter expired sessions when cleanup is true", async () => {
const now = Date.now();
const sessions: Cookie[] = [
{
id: "session-valid",
token: "token-valid",
loginName: "[email protected]",
creationTs: `${now}`,
expirationTs: `${now + 10000000}`,
changeTs: `${now}`,
},
{
id: "session-expired",
token: "token-expired",
loginName: "[email protected]",
creationTs: `${now - 10000000}`,
expirationTs: `${now - 1000}`,
changeTs: `${now - 10000000}`,
},
];
mockCookies.get.mockReturnValue({
value: JSON.stringify(sessions),
});
vi.mocked(timestampDate).mockImplementation((ts: any) => new Date(Number(ts.seconds) * 1000));
vi.mocked(timestampFromMs).mockImplementation((ms: number) => ({ seconds: BigInt(Math.floor(ms / 1000)) }) as any);
const result = await getAllSessionCookieIds(true);
expect(result).toEqual(["session-valid"]);
});
it("should return empty array when no sessions exist", async () => {
mockCookies.get.mockReturnValue(undefined);
const result = await getAllSessionCookieIds();
expect(result).toEqual([]);
});
});
describe("getAllSessions", () => {
it("should return all sessions", async () => {
const sessions: Cookie[] = [
{
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
},
{
id: "session-2",
token: "token-2",
loginName: "[email protected]",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000001000",
},
];
mockCookies.get.mockReturnValue({
value: JSON.stringify(sessions),
});
const result = await getAllSessions();
expect(result).toEqual(sessions);
});
it("should filter expired sessions when cleanup is true", async () => {
const now = Date.now();
const validSession: Cookie = {
id: "session-valid",
token: "token-valid",
loginName: "[email protected]",
creationTs: `${now}`,
expirationTs: `${now + 10000000}`,
changeTs: `${now}`,
};
const expiredSession: Cookie = {
id: "session-expired",
token: "token-expired",
loginName: "[email protected]",
creationTs: `${now - 10000000}`,
expirationTs: `${now - 1000}`,
changeTs: `${now - 10000000}`,
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([validSession, expiredSession]),
});
vi.mocked(timestampDate).mockImplementation((ts: any) => new Date(Number(ts.seconds) * 1000));
vi.mocked(timestampFromMs).mockImplementation((ms: number) => ({ seconds: BigInt(Math.floor(ms / 1000)) }) as any);
const result = await getAllSessions(true);
expect(result).toHaveLength(1);
expect(result[0].id).toBe("session-valid");
});
it("should return empty array when no sessions exist", async () => {
mockCookies.get.mockReturnValue(undefined);
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const result = await getAllSessions();
expect(result).toEqual([]);
expect(consoleSpy).toHaveBeenCalledWith("getAllSessions: No session cookie found, returning empty array");
consoleSpy.mockRestore();
});
});
describe("getMostRecentCookieWithLoginname", () => {
it("should return most recent session for loginName", async () => {
const session1: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
const session2: Cookie = {
id: "session-2",
token: "token-2",
loginName: "[email protected]",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000005000", // Most recent
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
const result = await getMostRecentCookieWithLoginname({
loginName: "[email protected]",
});
expect(result.id).toBe("session-2");
});
it("should return undefined when no matching session found", async () => {
const session1: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
const session2: Cookie = {
id: "session-2",
token: "token-2",
loginName: "[email protected]",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000005000",
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
await expect(
getMostRecentCookieWithLoginname({
loginName: "[email protected]",
}),
).resolves.toBeUndefined();
});
it("should filter by organization when provided", async () => {
const session1: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
organization: "org-1",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000005000",
};
const session2: Cookie = {
id: "session-2",
token: "token-2",
loginName: "[email protected]",
organization: "org-2",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000010000", // More recent but different org
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
const result = await getMostRecentCookieWithLoginname({
loginName: "[email protected]",
organization: "org-1",
});
expect(result.id).toBe("session-1");
expect(result.organization).toBe("org-1");
});
it("should return most recent session when no loginName provided", async () => {
const session1: Cookie = {
id: "session-1",
token: "token-1",
loginName: "[email protected]",
creationTs: "1700000000000",
expirationTs: "1800000000000",
changeTs: "1700000000000",
};
const session2: Cookie = {
id: "session-2",
token: "token-2",
loginName: "[email protected]",
creationTs: "1700000001000",
expirationTs: "1800000001000",
changeTs: "1700000005000",
};
mockCookies.get.mockReturnValue({
value: JSON.stringify([session1, session2]),
});
const result = await getMostRecentCookieWithLoginname({});
expect(result.id).toBe("session-2");
});
it("should return undefined when no session cookie exists", async () => {
mockCookies.get.mockReturnValue(undefined);
await expect(
getMostRecentCookieWithLoginname({
loginName: "[email protected]",
}),
).resolves.toBeUndefined();
});
});
});