From cb811d958599831632fac3b098a08412d840036c Mon Sep 17 00:00:00 2001 From: Timshel Date: Thu, 5 Sep 2024 19:22:37 +0200 Subject: [PATCH] Add 2fa authenticator test --- playwright/package-lock.json | 23 ++++++++++++ playwright/package.json | 1 + playwright/playwright.config.ts | 2 + playwright/tests/login.spec.ts | 65 +++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/playwright/package-lock.json b/playwright/package-lock.json index 24940070..96a4137b 100644 --- a/playwright/package-lock.json +++ b/playwright/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "mysql2": "^3.10.2", + "otpauth": "^9.3.2", "pg": "^8.12.0" }, "devDependencies": { @@ -19,6 +20,17 @@ "maildev": "github:timshel/maildev#3.0.0-rc1" } }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@playwright/test": { "version": "1.45.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", @@ -1500,6 +1512,17 @@ "node": ">= 0.8" } }, + "node_modules/otpauth": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.2.tgz", + "integrity": "sha512-KixtXWN9RGdS8WHPfDo7qsOYiivCbl+VeLBT+7HBTtJebBO6aXr/bpZXr+TwY2COecdY82VeBghm31mLYQVZlQ==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", diff --git a/playwright/package.json b/playwright/package.json index d99bff8a..e1b3a884 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "mysql2": "^3.10.2", + "otpauth": "^9.3.2", "pg": "^8.12.0" } } diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index 775bdfce..5e940f18 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -30,6 +30,8 @@ export default defineConfig({ /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: process.env.DOMAIN, browserName: 'firefox', + locale: 'en-GB', + timezoneId: 'Europe/London', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', }, diff --git a/playwright/tests/login.spec.ts b/playwright/tests/login.spec.ts index 3c99a47e..b8cfbc1a 100644 --- a/playwright/tests/login.spec.ts +++ b/playwright/tests/login.spec.ts @@ -1,9 +1,11 @@ import { test, expect, type Page, type TestInfo } from '@playwright/test'; +import * as OTPAuth from "otpauth"; import * as utils from "../global-utils"; import { createAccount, logUser } from './setups/user'; let users = utils.loadEnv(); +let totp; test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { await utils.startVaultwarden(browser, testInfo, {}); @@ -30,3 +32,66 @@ test('Account creation', async ({ page }) => { test('Master password login', async ({ page }) => { await logUser(test, page, users.user1); }); + +test('Authenticator 2fa', async ({ context, page }) => { + let totp; + + await test.step('Login', async () => { + await logUser(test, page, users.user1); + }); + + await test.step('Activate', async () => { + await page.getByRole('button', { name: users.user1.name }).click(); + await page.getByRole('menuitem', { name: 'Account settings' }).click(); + await page.getByLabel('Security').click(); + await page.getByRole('link', { name: 'Two-step login' }).click(); + await page.locator('li').filter({ hasText: 'Authenticator app Use an' }).getByRole('button').click(); + await page.getByLabel('Master password (required)').fill(users.user1.password); + await page.getByRole('button', { name: 'Continue' }).click(); + + const secret = await page.getByLabel('Key').innerText(); + totp = new OTPAuth.TOTP({ secret, period: 30 }); + + await page.getByLabel('3. Enter the resulting 6').fill(totp.generate()); + await page.getByRole('button', { name: 'Turn on' }).click(); + await page.getByRole('heading', { name: 'Turned on', exact: true }); + await page.getByLabel('Close').click(); + }) + + await test.step('logout', async () => { + await page.getByRole('button', { name: users.user1.name }).click(); + await page.getByRole('menuitem', { name: 'Log out' }).click(); + await expect(page.getByTestId("toast-title")).toHaveText("Logged out"); + await page.locator('#toast-container').getByRole('button').click(); + await expect(page.getByTestId("toast-title")).toHaveCount(0); + }); + + await test.step('login', async () => { + let timestamp = Date.now(); // Need to use the next token + timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000; + + await page.getByLabel(/Email address/).fill(users.user1.email); + await page.getByRole('button', { name: 'Continue' }).click(); + await page.getByLabel('Master password').fill(users.user1.password); + await page.getByRole('button', { name: 'Log in with master password' }).click(); + + await page.getByLabel('Verification code').fill(totp.generate({timestamp})); + await page.getByRole('button', { name: 'Continue' }).click(); + + await expect(page).toHaveTitle(/Vaults/); + }); + + await test.step('disable', async () => { + await page.getByRole('button', { name: 'Test' }).click(); + await page.getByRole('menuitem', { name: 'Account settings' }).click(); + await page.getByLabel('Security').click(); + await page.getByRole('link', { name: 'Two-step login' }).click(); + await page.locator('li').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); + await page.getByLabel('Master password (required)').click(); + await page.getByLabel('Master password (required)').fill(users.user1.password); + await page.getByRole('button', { name: 'Continue' }).click(); + await page.getByRole('button', { name: 'Turn off' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText(/Two-step login provider turned off/); + }); +});