From fba2a2e2ca445631bfee8347483877a5a9b92315 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 30 May 2023 19:53:49 +0530 Subject: [PATCH] test: Two Factor Auth (#9142) * Fix 2 Factor Auth * Add 2fa tests --- .../settings/DisableTwoFactorModal.tsx | 2 +- .../settings/EnableTwoFactorModal.tsx | 18 +- .../settings/security/two-factor-auth.tsx | 1 + apps/web/playwright/auth/auth-index.e2e.ts | 3 - apps/web/playwright/login.2fa.e2e.ts | 156 ++++++++++++++++++ 5 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 apps/web/playwright/login.2fa.e2e.ts diff --git a/apps/web/components/settings/DisableTwoFactorModal.tsx b/apps/web/components/settings/DisableTwoFactorModal.tsx index fd427e368c..987d4d6e81 100644 --- a/apps/web/components/settings/DisableTwoFactorModal.tsx +++ b/apps/web/components/settings/DisableTwoFactorModal.tsx @@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
-
-

{secret}

+

+ {secret} +

@@ -164,7 +166,11 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
- {errorMessage &&

{errorMessage}

} + {errorMessage && ( +

+ {errorMessage} +

+ )}
@@ -178,12 +184,16 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable - - diff --git a/apps/web/pages/settings/security/two-factor-auth.tsx b/apps/web/pages/settings/security/two-factor-auth.tsx index 811f9d2d03..9c0e74dd46 100644 --- a/apps/web/pages/settings/security/two-factor-auth.tsx +++ b/apps/web/pages/settings/security/two-factor-auth.tsx @@ -40,6 +40,7 @@ const TwoFactorAuthView = () => { {!isCalProvider && }
diff --git a/apps/web/playwright/auth/auth-index.e2e.ts b/apps/web/playwright/auth/auth-index.e2e.ts index fac393621f..19c568bb14 100644 --- a/apps/web/playwright/auth/auth-index.e2e.ts +++ b/apps/web/playwright/auth/auth-index.e2e.ts @@ -1,5 +1,4 @@ import { test } from "../lib/fixtures"; -import { todo } from "../lib/testUtils"; test.describe("Can signup from a team invite", async () => { test.beforeEach(async ({ users }) => { @@ -96,5 +95,3 @@ test.describe("Can signup from a team invite", async () => { expect(createdUser.teams[0].accepted).toBe(true);*/ }); }); - -todo("Can login using 2FA"); diff --git a/apps/web/playwright/login.2fa.e2e.ts b/apps/web/playwright/login.2fa.e2e.ts new file mode 100644 index 0000000000..2a869e5763 --- /dev/null +++ b/apps/web/playwright/login.2fa.e2e.ts @@ -0,0 +1,156 @@ +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 page.press('input[name="2fa6"]', "Enter"); + 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 page.press('input[name="2fa6"]', "Enter"); + + 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 page.click('[data-testid="enable-2fa"]'); + 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]); +}