Files
zitadel/apps/login/src/lib/session.test.ts
T
Max PeintnerandGitHub ba3a45bee4 fix(login): treat user-verified passkey as fulfilling MFA in session validity check (#12575)
# Which Problems Are Solved

Users with a passkey plus an additional configured second factor (TOTP /
OTP Email / OTP SMS) get stuck on the passkey verification page: the
WebAuthn ceremony succeeds, but the continue button just spins and the
user is redirected back to `/passkey` indefinitely. Re-registering the
passkey or switching to a private window does not help.

Since the MFA hardening of `isSessionValid` (56f4798), a session is only
considered valid if one of the user's *configured* MFA methods (TOTP,
OTP Email, OTP SMS, U2F) was verified within the session. A passkey
login sets `factors.webAuthN.verifiedAt` with `userVerified: true`, but
only the U2F method reuses that factor — so a passkey session for a user
whose second factor is TOTP or OTP is deemed invalid at flow completion.

This contradicts the login flow itself: `checkMFAFactors` intentionally
never prompts for a second factor after a passkey login ("escape further
checks if user has authenticated with passkey"). The result is a loop:

1. Passkey is verified on `/passkey` → `sendPasskey` →
`completeAuthFlow` → `loginWithOIDCAndSession`
2. `isSessionValid` returns `false` because the configured TOTP/OTP
factor was never checked in this session
3. The re-authentication branch calls `sendLoginname`, which resolves
the user's preferred method (passkey) and returns a redirect to
`/passkey` — back to step 1, with no way forward

# How the Problems Are Solved

Treat a user-verified passkey authentication as fulfilling the MFA
requirement in `isSessionValid`, consistent with the existing passkey
escape in `checkMFAFactors`. A passkey with user verification is
inherently multi-factor (possession + biometrics/PIN).

- `isSessionValid` now derives `hasAuthenticatedWithPasskey` from
`factors.webAuthN.verifiedAt && factors.webAuthN.userVerified` and
accepts it alongside the configured-factor checks (`totpValid ||
otpEmailValid || otpSmsValid || u2fValid`).
- A presence-only WebAuthn assertion (`userVerified: false`, i.e. a
U2F-style second-factor check) does **not** count, so password-based
sessions still require their configured second factor — the original
security fix remains intact.
- Adds regression tests: a user-verified passkey session with a
configured-but-unverified TOTP factor is valid; a presence-only WebAuthn
session with an unverified TOTP factor remains invalid.
2026-08-11 14:16:45 +02:00

1684 lines
57 KiB
TypeScript

/**
* Unit tests for the isSessionValid function.
*
* This test suite covers the comprehensive session validation logic including:
* - Session expiration checks
* - User presence validation
* - Authentication factor verification (password, passkey, IDP)
* - MFA validation using the shared shouldEnforceMFA function from verify-helper
* - Passkey authentication inherently satisfies MFA requirements
* - MFA validation with configured authentication methods (TOTP, OTP Email/SMS, U2F)
* - MFA validation with login settings (forceMfa, forceMfaLocalOnly)
* - Email verification when EMAIL_VERIFICATION environment variable is enabled
* - Edge cases like sessions without expiration date
*/
import { Code, ConnectError } from "@connectrpc/connect";
import { timestampDate } from "@zitadel/client";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import * as cookiesModule from "./cookies";
import { isSessionValid, loadMostRecentSession } from "./session";
import * as verifyHelperModule from "./verify-helper";
import * as zitadelModule from "./zitadel";
vi.mock("@zitadel/client", () => ({
timestampDate: vi.fn(),
}));
vi.mock("./zitadel", () => ({
listAuthenticationMethodTypes: vi.fn(),
getLoginSettings: vi.fn(),
getUserByID: vi.fn(),
getSession: vi.fn(),
}));
vi.mock("./cookies", () => ({
getMostRecentCookieWithLoginname: vi.fn(),
}));
vi.mock("./verify-helper", () => ({
shouldEnforceMFA: vi.fn(),
}));
const originalEnv = process.env;
describe("isSessionValid", () => {
const mockServiceUrl = "https://zitadel-abc123.zitadel.cloud";
const mockUserId = "test-user-id";
const mockOrganizationId = "test-org-id";
beforeEach(() => {
vi.clearAllMocks();
process.env = { ...originalEnv };
// @ts-ignore - delete is OK for test environment variables
delete process.env.EMAIL_VERIFICATION;
// Setup timestampDate mock to match actual behavior:
// - Returns Invalid Date for malformed timestamps (empty object, invalid seconds)
// - Throws for null/undefined
// - Returns valid Date for proper timestamps
vi.mocked(timestampDate).mockImplementation((timestamp: any) => {
if (timestamp === null || timestamp === undefined) {
throw new TypeError(`Cannot read properties of ${timestamp} (reading 'seconds')`);
}
if (!timestamp.seconds || typeof timestamp.seconds !== "bigint") {
return new Date("invalid"); // Returns Invalid Date for malformed timestamps
}
return new Date(Number(timestamp.seconds) * 1000);
});
});
afterEach(() => {
process.env = originalEnv;
});
const createMockTimestamp = (offsetMs = 3600000): any => ({
seconds: BigInt(Math.floor((Date.now() + offsetMs) / 1000)),
});
const createMockSession = (overrides: any = {}): any => {
const futureTimestamp = createMockTimestamp();
const defaultSession = {
id: "session-id",
expirationDate: futureTimestamp,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: futureTimestamp,
},
password: {
verifiedAt: futureTimestamp,
},
},
...overrides,
};
return defaultSession;
};
describe("when session has no user", () => {
test("should return false and log warning", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const session = createMockSession({
factors: undefined,
});
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(result).toBe(false);
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
describe("when session is expired", () => {
test("should return false and log warning", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const session = createMockSession({
expirationDate: createMockTimestamp(-3600000), // 1 hour ago
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] Session is expired", expect.any(String));
consoleSpy.mockRestore();
});
});
describe("when session has no valid authentication factors", () => {
test("should return false when no password, passkey, or IDP verification", async () => {
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: createMockTimestamp(),
},
// No password, webAuthN, or intent factors
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
});
});
describe("MFA validation with configured authentication methods", () => {
test("should return true when TOTP is configured and verified with MFA required", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
totp: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return true when TOTP is configured but not verified and MFA is not required", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// No TOTP verification
},
});
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return false when TOTP is configured but not verified and MFA is required", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// No TOTP verification
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] MFA is required but not valid");
consoleSpy.mockRestore();
});
test("should return true when OTP Email is configured and verified with MFA required", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
otpEmail: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.OTP_EMAIL],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return true when U2F is configured and verified", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
webAuthN: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.U2F],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return true when multiple auth methods are configured and one is verified with MFA required", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
otpEmail: {
verifiedAt: verifiedTimestamp,
},
// TOTP not verified
},
});
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return true when session has only password and MFA is not required by policy", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// No MFA factors verified
},
});
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
// User has no MFA methods configured
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return false when user has PASSWORD and TOTP configured but only password verified and MFA not required by policy", async () => {
// User has TOTP configured as an MFA method.
// Even though MFA is not required by policy, the user's configured MFA
// factor must be verified because they have it set up.
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// TOTP is configured but NOT verified
},
});
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
// User has TOTP configured
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] MFA is required but not valid");
consoleSpy.mockRestore();
});
test("should return false when user has PASSWORD and TOTP configured but only password verified and MFA IS required", async () => {
// This is the counterpart test to ensure MFA is still enforced when required
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// TOTP is configured but NOT verified
// totp: undefined (no verifiedAt)
},
});
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.TOTP],
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] MFA is required but not valid");
consoleSpy.mockRestore();
});
test("REGRESSION TEST: user with only PASSWORD factor should be valid when MFA not required", async () => {
// User has only PASSWORD configured, no MFA methods at all.
// Session should be valid since no MFA factors need verification.
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// Explicitly no MFA factors at all
totp: undefined,
otpEmail: undefined,
otpSms: undefined,
webAuthN: undefined,
intent: undefined,
},
});
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
// User has only PASSWORD, no MFA methods
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// This should be true - user has no MFA methods configured
expect(result).toBe(true);
});
test("should return false when user has TOTP configured but not verified, even if MFA not required by policy", async () => {
// User has TOTP configured as an MFA method.
// Even though MFA is not required by policy, the configured MFA
// factor must be verified.
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// User has TOTP configured but not verified
totp: undefined,
},
});
// MFA is NOT required by policy
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
// User has TOTP configured
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// With the new logic, MFA must be verified if user has MFA methods
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] MFA is required but not valid");
consoleSpy.mockRestore();
});
test("should return true when user has TOTP configured and verified, even if MFA not required by policy", async () => {
// User has TOTP configured as an MFA method and it is verified on the session.
// Even though MFA is not required by policy, the configured MFA factor
// is verified so the session should be valid.
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
totp: {
verifiedAt: verifiedTimestamp,
},
},
});
// MFA is NOT required by policy
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
// User has TOTP configured
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
});
describe("MFA validation with login settings (no configured auth methods)", () => {
test("should return true when MFA is not forced and no auth methods configured", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return false when MFA is forced but no factors are verified", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// No MFA factors verified
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] MFA is required but not valid");
consoleSpy.mockRestore();
});
test("should return true when MFA is forced and TOTP is verified", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
totp: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return true when forceMfaLocalOnly is enabled and WebAuthn is verified", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
webAuthN: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: true,
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return false when forceMfaLocalOnly is enabled for password authentication but MFA not satisfied", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
// No MFA factors verified
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.TOTP],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: true,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalledWith({
serviceConfig: { baseUrl: mockServiceUrl },
userId: mockUserId,
});
consoleSpy.mockRestore();
});
});
describe("email verification", () => {
test("should return false when EMAIL_VERIFICATION is enabled and email is not verified", async () => {
process.env.EMAIL_VERIFICATION = "true";
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
vi.mocked(zitadelModule.getUserByID).mockResolvedValue({
user: {
type: {
case: "human",
value: {
email: {
email: "[email protected]",
isVerified: false,
},
},
},
},
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] Email is not verified");
consoleSpy.mockRestore();
});
test("should return true when EMAIL_VERIFICATION is enabled and email is verified", async () => {
process.env.EMAIL_VERIFICATION = "true";
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
vi.mocked(zitadelModule.getUserByID).mockResolvedValue({
user: {
type: {
case: "human",
value: {
email: {
email: "[email protected]",
isVerified: true,
},
},
},
},
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return true when EMAIL_VERIFICATION is disabled", async () => {
// EMAIL_VERIFICATION is not set, so it's disabled by default
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
// getUserByID should not be called when EMAIL_VERIFICATION is disabled
expect(zitadelModule.getUserByID).not.toHaveBeenCalled();
});
});
describe("passkey authentication", () => {
test("should return true when authenticated with passkey (WebAuthn)", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
webAuthN: {
verifiedAt: verifiedTimestamp,
},
// No password factor
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return true when authenticated with user-verified passkey even if TOTP is configured but not verified", async () => {
// Regression: a user with passkey + password + TOTP got stuck in a redirect loop on
// /passkey — the flow never prompts for TOTP after a passkey login (checkMFAFactors
// escapes), but isSessionValid deemed the session invalid because the configured
// TOTP factor was not verified. A user-verified passkey is inherently multi-factor.
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
webAuthN: {
verifiedAt: verifiedTimestamp,
userVerified: true,
},
// No password factor, TOTP configured but not verified in this session
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [
AuthenticationMethodType.PASSWORD,
AuthenticationMethodType.PASSKEY,
AuthenticationMethodType.TOTP,
],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return false when WebAuthn was presence-only (no userVerified) and configured TOTP is not verified", async () => {
// A presence-only WebAuthn assertion (U2F-style, userVerified=false) is not a passkey
// login and must not bypass the configured-MFA requirement.
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
webAuthN: {
verifiedAt: verifiedTimestamp,
userVerified: false,
},
// TOTP configured but not verified in this session
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.PASSWORD, AuthenticationMethodType.TOTP],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] MFA is required but not valid");
consoleSpy.mockRestore();
});
});
describe("IDP authentication", () => {
test("should return true when authenticated with IDP intent and no MFA required", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
intent: {
verifiedAt: verifiedTimestamp,
},
// No password factor
},
});
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
});
test("should return false when authenticated with IDP intent but MFA required and not satisfied", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
intent: {
verifiedAt: verifiedTimestamp,
},
// No password factor, no MFA factors verified
},
});
// shouldEnforceMFA returns true (MFA is required for this session)
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
// User has MFA methods configured but none verified
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalledWith({
serviceConfig: { baseUrl: mockServiceUrl },
userId: mockUserId,
});
});
test("should return false when authenticated with IDP intent and forceMfaLocalOnly but user has unverified MFA methods", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
intent: {
verifiedAt: verifiedTimestamp,
},
// No password factor, no MFA factors verified
},
});
// shouldEnforceMFA returns false (IDP bypasses forceMfaLocalOnly)
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: true,
} as any);
// User has MFA methods configured but none verified
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// MFA methods must be verified regardless of policy
expect(result).toBe(false);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
// listAuthenticationMethodTypes is always called now
expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith("[Session] MFA is required but not valid");
consoleSpy.mockRestore();
});
test("should return true when authenticated with IDP intent and MFA required and satisfied", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
intent: {
verifiedAt: verifiedTimestamp,
},
totp: {
verifiedAt: verifiedTimestamp,
},
},
});
// Organization enforces MFA
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
// User has TOTP configured and verified
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.TOTP],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
});
describe("passkey authentication", () => {
test("should return false when authenticated with passkey but user has unverified MFA methods", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
webAuthN: {
verifiedAt: verifiedTimestamp,
},
// No password factor, no additional MFA factors verified
},
});
// shouldEnforceMFA returns false (passkey satisfies MFA requirements)
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
// User has TOTP configured but not verified
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.TOTP],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// MFA methods must be verified regardless of policy or passkey
expect(result).toBe(false);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
// listAuthenticationMethodTypes is always called now
expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith("[Session] MFA is required but not valid");
consoleSpy.mockRestore();
});
});
describe("edge cases", () => {
test("should handle session without expiration date", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
expirationDate: undefined, // No expiration date
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should handle session with null expirationDate", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
expirationDate: null,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should handle session expiring exactly now", async () => {
const verifiedTimestamp = createMockTimestamp();
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
// Create timestamp for current moment (0ms offset)
const nowTimestamp = createMockTimestamp(0);
const session = createMockSession({
expirationDate: nowTimestamp,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// Session expiring exactly now should be considered expired
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("[Session] Session is expired", expect.any(String));
consoleSpy.mockRestore();
});
test("should handle API errors gracefully when fetching login settings", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
// Simulate API error
vi.mocked(zitadelModule.getLoginSettings).mockRejectedValue(new Error("API connection failed"));
// Should throw or handle error appropriately
await expect(isSessionValid({ serviceUrl: mockServiceUrl, session })).rejects.toThrow();
});
test("should handle API errors gracefully when listing auth method types", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
// Simulate API error
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockRejectedValue(new Error("API connection failed"));
// Should throw or handle error appropriately
await expect(isSessionValid({ serviceUrl: mockServiceUrl, session })).rejects.toThrow();
});
test("should handle malformed timestamp in session", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
expirationDate: { seconds: "invalid" } as any,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
// Malformed timestamps are handled gracefully, returning false for invalid sessions
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
consoleSpy.mockRestore();
});
test("should handle session with all MFA factors verified", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
totp: {
verifiedAt: verifiedTimestamp,
},
otpEmail: {
verifiedAt: verifiedTimestamp,
},
otpSms: {
verifiedAt: verifiedTimestamp,
},
webAuthN: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [
AuthenticationMethodType.TOTP,
AuthenticationMethodType.OTP_EMAIL,
AuthenticationMethodType.OTP_SMS,
AuthenticationMethodType.U2F,
],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: true,
forceMfaLocalOnly: false,
} as any);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should handle session with very old verifiedAt timestamps", async () => {
// Timestamp from 10 years ago
const oldTimestamp = {
seconds: BigInt(Math.floor((Date.now() - 10 * 365 * 24 * 60 * 60 * 1000) / 1000)),
};
const futureExpiration = createMockTimestamp();
const session = createMockSession({
expirationDate: futureExpiration,
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: oldTimestamp,
},
password: {
verifiedAt: oldTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
// Old verification timestamps are still valid as long as session hasn't expired
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should handle session with future verifiedAt timestamps", async () => {
// Timestamp 1 hour in the future
const futureTimestamp = createMockTimestamp(3600000);
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: futureTimestamp,
},
password: {
verifiedAt: futureTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
// Future timestamps should still be considered valid
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should handle concurrent calls to isSessionValid", async () => {
const verifiedTimestamp = createMockTimestamp();
const session = createMockSession({
factors: {
user: {
id: mockUserId,
organizationId: mockOrganizationId,
loginName: "[email protected]",
displayName: "Test User",
verifiedAt: verifiedTimestamp,
},
password: {
verifiedAt: verifiedTimestamp,
},
},
});
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [],
} as any);
vi.mocked(zitadelModule.getLoginSettings).mockResolvedValue({
forceMfa: false,
forceMfaLocalOnly: false,
} as any);
// Make multiple concurrent calls
const promises = Array(5)
.fill(null)
.map(() => isSessionValid({ serviceUrl: mockServiceUrl, session }));
const results = await Promise.all(promises);
// All should return same result
expect(results.every((r) => r === true)).toBe(true);
});
});
});
describe("loadMostRecentSession", () => {
const serviceConfig = { baseUrl: "https://zitadel-abc123.zitadel.cloud" };
const sessionParams = { loginName: "[email protected]", organization: "test-org-id" };
const cookie = { id: "session-id", token: "session-token", loginName: "[email protected]", organization: "test-org-id" };
beforeEach(() => {
vi.clearAllMocks();
});
test("returns undefined without calling getSession when no cookie is found", async () => {
vi.mocked(cookiesModule.getMostRecentCookieWithLoginname).mockResolvedValue(undefined as any);
const result = await loadMostRecentSession({ serviceConfig, sessionParams });
expect(result).toBeUndefined();
expect(zitadelModule.getSession).not.toHaveBeenCalled();
});
test("returns the session when getSession succeeds", async () => {
const session = { id: "session-id", factors: { user: { id: "user-id" } } };
vi.mocked(cookiesModule.getMostRecentCookieWithLoginname).mockResolvedValue(cookie as any);
vi.mocked(zitadelModule.getSession).mockResolvedValue({ session } as any);
const result = await loadMostRecentSession({ serviceConfig, sessionParams });
expect(result).toBe(session);
expect(zitadelModule.getSession).toHaveBeenCalledWith({
serviceConfig,
sessionId: cookie.id,
sessionToken: cookie.token,
});
});
test("returns undefined instead of throwing when getSession rejects with NotFound (stale cookie)", async () => {
// The `sessions` cookie can outlive the server-side session, so getSession may reject with
// a NotFound ConnectError. loadMostRecentSession must treat that the same as no session.
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const notFound = new ConnectError("Session does not exist (QUERY-SFeaa)", Code.NotFound);
vi.mocked(cookiesModule.getMostRecentCookieWithLoginname).mockResolvedValue(cookie as any);
vi.mocked(zitadelModule.getSession).mockRejectedValue(notFound);
const result = await loadMostRecentSession({ serviceConfig, sessionParams });
expect(result).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith("[Session] Could not load most recent session", notFound);
consoleSpy.mockRestore();
});
test("re-throws non-NotFound ConnectErrors (real failures must propagate)", async () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const unavailable = new ConnectError("backend unavailable", Code.Unavailable);
vi.mocked(cookiesModule.getMostRecentCookieWithLoginname).mockResolvedValue(cookie as any);
vi.mocked(zitadelModule.getSession).mockRejectedValue(unavailable);
await expect(loadMostRecentSession({ serviceConfig, sessionParams })).rejects.toBe(unavailable);
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
test("re-throws non-ConnectError rejections (only coded NotFound is treated as no session)", async () => {
// The guard is `instanceof ConnectError && code === NotFound`; anything that is not a
// ConnectError (e.g. an unexpected runtime error) must not be mistaken for a stale
// cookie and must propagate.
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const unexpected = new Error("boom");
vi.mocked(cookiesModule.getMostRecentCookieWithLoginname).mockResolvedValue(cookie as any);
vi.mocked(zitadelModule.getSession).mockRejectedValue(unexpected);
await expect(loadMostRecentSession({ serviceConfig, sessionParams })).rejects.toBe(unexpected);
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
});