Files
zitadel/apps/login/src/lib/session.test.ts
T
9f1119823b fix(login): cleanup server logs (#11195)
Closes #11184

# Which Problems Are Solved

- loginName: This information is already retrievable from the URL
parameters during the authentication flow, so logging it presented no
additional exposure.
- idpIntent: This was being logged to the server console (server-side
logs), so it was never exposed to the client/browser.
These changes simply clean up the server logs to prevent unnecessary
data noise.

# How the Problems Are Solved

- Removed idpIntent logging from 
`apps/login/src/lib/server/idp-intent.ts`
- Removed loginName logging from 
`apps/login/src/lib/server/password.ts`

---------

Co-authored-by: Livio Spring <[email protected]>
2026-01-05 13:46:09 +00:00

1450 lines
48 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 { 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 { isSessionValid } 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(),
}));
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);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
test("should return true when user has PASSWORD and TOTP configured but only password verified and MFA not required", async () => {
// This test specifically covers the original bug scenario:
// - User has PASSWORD and TOTP configured (would show up in listAuthenticationMethodTypes)
// - User has only verified password, not TOTP
// - MFA is not required by policy
// - Session should be valid (this was the bug - it was returning false)
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 - this is the key part
// totp: undefined (no verifiedAt)
},
});
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 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 () => {
// This test specifically verifies the original bug is fixed
// Original bug: A user with only PASSWORD authentication would be invalid
// because the code checked if authMethods.length > 0 (which included PASSWORD)
// and then required MFA verification even when MFA was not required by policy
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);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// This should be true - if it's false, the original bug still exists
expect(result).toBe(true);
});
test("DEMONSTRATION: how the original bug would manifest with old logic", async () => {
// This test demonstrates the original problematic scenario:
// 1. listAuthenticationMethodTypes returns [PASSWORD, TOTP]
// 2. Old logic would check authMethods.length > 0 (true because PASSWORD is included)
// 3. Old logic would then require MFA verification regardless of policy
// 4. User has only password verified, no TOTP
// 5. Session would be marked invalid even though MFA is not required
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);
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// With our fix, this should be true (session is valid)
// With the old logic, this would have been false (bug)
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);
});
});
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 true when authenticated with IDP intent and forceMfaLocalOnly (IDP bypasses local-only MFA)", 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 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 });
expect(result).toBe(true);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
// Should not call listAuthenticationMethodTypes since shouldEnforceMFA returned false
expect(zitadelModule.listAuthenticationMethodTypes).not.toHaveBeenCalled();
});
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 true when authenticated with passkey and MFA required (passkey satisfies MFA)", 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, no additional MFA factors
},
});
// 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 MFA methods configured but none verified (passkey should satisfy MFA)
vi.mocked(zitadelModule.listAuthenticationMethodTypes).mockResolvedValue({
authMethodTypes: [AuthenticationMethodType.TOTP],
} 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,
});
// Should not call listAuthenticationMethodTypes since shouldEnforceMFA returned false
expect(zitadelModule.listAuthenticationMethodTypes).not.toHaveBeenCalled();
});
});
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);
});
});
});