mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-23 07:52:15 +00:00
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]>
1450 lines
48 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|