155 lines
5.9 KiB
TypeScript
155 lines
5.9 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import { expect } from "@playwright/test";
|
|
import { authenticator } from "otplib";
|
|
|
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
|
|
|
import { test } from "./lib/fixtures";
|
|
|
|
test.describe.configure({ mode: "parallel" });
|
|
|
|
// a test to logout requires both a succesfull login as logout, to prevent
|
|
// a doubling of tests failing on logout & logout, we can group them.
|
|
test.describe("2FA Tests", async () => {
|
|
test.afterAll(async ({ users }) => {
|
|
await users.deleteAll();
|
|
});
|
|
test("should allow a user to enable 2FA and login using 2FA", async ({ page, users }) => {
|
|
// log in trail user
|
|
const user = await test.step("Enable 2FA", async () => {
|
|
const user = await users.create();
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const userPassword = user.username!;
|
|
await user.login();
|
|
|
|
// expects the home page for an authorized user
|
|
await page.goto("/settings/security/two-factor-auth");
|
|
await page.click(`[data-testid=two-factor-switch]`);
|
|
await page.fill('input[name="password"]', userPassword);
|
|
await page.press('input[name="password"]', "Enter");
|
|
const secret = await page.locator(`[data-testid=two-factor-secret]`).textContent();
|
|
expect(secret).toHaveLength(32);
|
|
await page.click('[data-testid="goto-otp-screen"]');
|
|
|
|
/**
|
|
* Try a wrong code and test that wrong code is rejected.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
await fillOtp({ page, secret: "123456", noRetry: true });
|
|
await expect(page.locator('[data-testid="error-submitting-code"]')).toBeVisible();
|
|
|
|
await fillOtp({
|
|
page,
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
secret: secret!,
|
|
});
|
|
|
|
await expect(page.locator(`[data-testid=two-factor-switch][data-state="checked"]`)).toBeVisible();
|
|
|
|
return user;
|
|
});
|
|
|
|
await test.step("Logout", async () => {
|
|
await page.goto("/auth/logout");
|
|
});
|
|
|
|
await test.step("Login with 2FA enabled", async () => {
|
|
await user.login();
|
|
const userWith2FaSecret = await prisma?.user.findFirst({
|
|
where: {
|
|
id: user.id,
|
|
},
|
|
});
|
|
|
|
const secret = symmetricDecrypt(
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
userWith2FaSecret!.twoFactorSecret!,
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
process.env.CALENDSO_ENCRYPTION_KEY!
|
|
);
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
await fillOtp({ page, secret: secret! });
|
|
await Promise.all([
|
|
page.press('input[name="2fa6"]', "Enter"),
|
|
page.waitForResponse("**/api/auth/callback/credentials**"),
|
|
]);
|
|
const shellLocator = page.locator(`[data-testid=dashboard-shell]`);
|
|
|
|
// expects the home page for an authorized user
|
|
await page.goto("/");
|
|
await expect(shellLocator).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test("should allow a user to disable 2FA", async ({ page, users }) => {
|
|
// log in trail user
|
|
const user = await test.step("Enable 2FA", async () => {
|
|
const user = await users.create();
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const userPassword = user.username!;
|
|
await user.login();
|
|
|
|
// expects the home page for an authorized user
|
|
await page.goto("/settings/security/two-factor-auth");
|
|
await page.click(`[data-testid=two-factor-switch][data-state="unchecked"]`);
|
|
await page.fill('input[name="password"]', userPassword);
|
|
await page.press('input[name="password"]', "Enter");
|
|
const secret = await page.locator(`[data-testid=two-factor-secret]`).textContent();
|
|
expect(secret).toHaveLength(32);
|
|
await page.click('[data-testid="goto-otp-screen"]');
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
await fillOtp({ page, secret: secret! });
|
|
|
|
await expect(page.locator(`[data-testid=two-factor-switch][data-state="checked"]`)).toBeVisible();
|
|
|
|
return user;
|
|
});
|
|
|
|
await test.step("Disable 2FA", async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const userPassword = user.username!;
|
|
|
|
// expects the home page for an authorized user
|
|
await page.goto("/settings/security/two-factor-auth");
|
|
await page.click(`[data-testid=two-factor-switch][data-state="checked"]`);
|
|
await page.fill('input[name="password"]', userPassword);
|
|
|
|
const userWith2FaSecret = await prisma?.user.findFirst({
|
|
where: {
|
|
id: user.id,
|
|
},
|
|
});
|
|
|
|
const secret = symmetricDecrypt(
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
userWith2FaSecret!.twoFactorSecret!,
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
process.env.CALENDSO_ENCRYPTION_KEY!
|
|
);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
await fillOtp({ page, secret: secret! });
|
|
await page.click('[data-testid="disable-2fa"]');
|
|
await expect(page.locator(`[data-testid=two-factor-switch][data-state="unchecked"]`)).toBeVisible();
|
|
|
|
return user;
|
|
});
|
|
});
|
|
});
|
|
|
|
async function fillOtp({ page, secret, noRetry }: { page: Page; secret: string; noRetry?: boolean }) {
|
|
let token = authenticator.generate(secret);
|
|
if (!noRetry && !authenticator.check(token, secret)) {
|
|
console.log("Token expired, Renerating.");
|
|
// Maybe token was just about to expire, try again just once more
|
|
token = authenticator.generate(secret);
|
|
}
|
|
await page.fill('input[name="2fa1"]', token[0]);
|
|
await page.fill('input[name="2fa2"]', token[1]);
|
|
await page.fill('input[name="2fa3"]', token[2]);
|
|
await page.fill('input[name="2fa4"]', token[3]);
|
|
await page.fill('input[name="2fa5"]', token[4]);
|
|
await page.fill('input[name="2fa6"]', token[5]);
|
|
}
|