mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-13 20:15:58 +03:00
SSO using OpenID Connect (#3899)
* Add SSO functionality using OpenID Connect Co-authored-by: Pablo Ovelleiro Corral <mail@pablo.tools> Co-authored-by: Stuart Heap <sheap13@gmail.com> Co-authored-by: Alex Moore <skiepp@my-dockerfarm.cloud> Co-authored-by: Brian Munro <brian.alexander.munro@gmail.com> Co-authored-by: Jacques B. <timshel@github.com> * Improvements and error handling * Stop rolling device token * Add playwright tests * Activate PKCE by default * Ensure result order when searching for sso_user * add SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION * Toggle SSO button in scss * Base64 encode state before sending it to providers * Prevent disabled User from SSO login * Review fixes * Remove unused UserOrganization.invited_by_email * Split SsoUser::find_by_identifier_or_email * api::Accounts::verify_password add the policy even if it's ignored * Disable signups if SSO_ONLY is activated * Add verifiedDate to organizations::get_org_domain_sso_details * Review fixes * Remove OrganizationId guard from get_master_password_policy * Add wrapper type OIDCCode OIDCState OIDCIdentifier * Membership::confirm_user_invitations fix and tests * Allow set-password only if account is unitialized * Review fixes * Prevent accepting another user invitation * Log password change event on SSO account creation * Unify master password policy resolution * Upgrade openidconnect to 4.0.0 * Revert "Remove unused UserOrganization.invited_by_email" This reverts commit 548e19995e141314af98a10d170ea7371f02fab4. * Process org enrollment in accounts::post_set_password * Improve tests * Pass the claim invited_by_email in case it was not in db * Add Slack configuration hints * Fix playwright tests * Skip broken tests * Add sso identifier in admin user panel * Remove duplicate expiration check, add a log * Augment mobile refresh_token validity * Rauthy configuration hints * Fix playwright tests * Playwright upgrade and conf improvement * Playwright tests improvements * 2FA email and device creation change * Fix and improve Playwright tests * Minor improvements * Fix enforceOnLogin org policies * Run playwright sso tests against correct db * PKCE should now work with Zitadel * Playwright upgrade maildev to use MailBuffer.expect * Upgrades playwright tests deps * Check email_verified in id_token and user_info * Add sso verified endpoint for v2025.6.0 * Fix playwright tests * Create a separate sso_client * Upgrade openidconnect to 4.0.1 * Server settings for login fields toggle * Use only css for login fields * Fix playwright test * Review fix * More review fix * Perform same checks when setting kdf --------- Co-authored-by: Felix Eckhofer <felix@eckhofer.com> Co-authored-by: Pablo Ovelleiro Corral <mail@pablo.tools> Co-authored-by: Stuart Heap <sheap13@gmail.com> Co-authored-by: Alex Moore <skiepp@my-dockerfarm.cloud> Co-authored-by: Brian Munro <brian.alexander.munro@gmail.com> Co-authored-by: Jacques B. <timshel@github.com> Co-authored-by: Timshel <timshel@480s>
This commit is contained in:
92
playwright/tests/setups/2fa.ts
Normal file
92
playwright/tests/setups/2fa.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { expect, type Page, Test } from '@playwright/test';
|
||||
import { type MailBuffer } from 'maildev';
|
||||
import * as OTPAuth from "otpauth";
|
||||
|
||||
import * as utils from '../../global-utils';
|
||||
|
||||
export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP {
|
||||
return await test.step('Activate TOTP 2FA', async () => {
|
||||
await page.getByRole('button', { name: user.name }).click();
|
||||
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||
await page.getByRole('link', { name: 'Security' }).click();
|
||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
||||
await page.getByLabel('Master password (required)').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
const secret = await page.getByLabel('Key').innerText();
|
||||
let totp = new OTPAuth.TOTP({ secret, period: 30 });
|
||||
|
||||
await page.getByLabel(/Verification code/).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();
|
||||
|
||||
return totp;
|
||||
})
|
||||
}
|
||||
|
||||
export async function disableTOTP(test: Test, page: Page, user: { password: string }) {
|
||||
await test.step('Disable TOTP 2FA', async () => {
|
||||
await page.getByRole('button', { name: 'Test' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||
await page.getByRole('link', { name: 'Security' }).click();
|
||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
||||
await page.getByLabel('Master password (required)').click();
|
||||
await page.getByLabel('Master password (required)').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByRole('button', { name: 'Turn off' }).click();
|
||||
await page.getByRole('button', { name: 'Yes' }).click();
|
||||
await utils.checkNotification(page, 'Two-step login provider turned off');
|
||||
});
|
||||
}
|
||||
|
||||
export async function activateEmail(test: Test, page: Page, user: { name: string, password: string }, mailBuffer: MailBuffer) {
|
||||
await test.step('Activate Email 2FA', async () => {
|
||||
await page.getByRole('button', { name: user.name }).click();
|
||||
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||
await page.getByRole('link', { name: 'Security' }).click();
|
||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||
await page.locator('bit-item').filter({ hasText: 'Email Email Enter a code sent' }).getByRole('button').click();
|
||||
await page.getByLabel('Master password (required)').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByRole('button', { name: 'Send email' }).click();
|
||||
});
|
||||
|
||||
let code = await retrieveEmailCode(test, page, mailBuffer);
|
||||
|
||||
await test.step('input code', async () => {
|
||||
await page.getByLabel('2. Enter the resulting 6').fill(code);
|
||||
await page.getByRole('button', { name: 'Turn on' }).click();
|
||||
await page.getByRole('heading', { name: 'Turned on', exact: true });
|
||||
});
|
||||
}
|
||||
|
||||
export async function retrieveEmailCode(test: Test, page: Page, mailBuffer: MailBuffer): string {
|
||||
return await test.step('retrieve code', async () => {
|
||||
const codeMail = await mailBuffer.expect((mail) => mail.subject.includes("Login Verification Code"));
|
||||
const page2 = await page.context().newPage();
|
||||
await page2.setContent(codeMail.html);
|
||||
const code = await page2.getByTestId("2fa").innerText();
|
||||
await page2.close();
|
||||
return code;
|
||||
});
|
||||
}
|
||||
|
||||
export async function disableEmail(test: Test, page: Page, user: { password: string }) {
|
||||
await test.step('Disable Email 2FA', async () => {
|
||||
await page.getByRole('button', { name: 'Test' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||
await page.getByRole('link', { name: 'Security' }).click();
|
||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||
await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click();
|
||||
await page.getByLabel('Master password (required)').click();
|
||||
await page.getByLabel('Master password (required)').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByRole('button', { name: 'Turn off' }).click();
|
||||
await page.getByRole('button', { name: 'Yes' }).click();
|
||||
|
||||
await utils.checkNotification(page, 'Two-step login provider turned off');
|
||||
});
|
||||
}
|
7
playwright/tests/setups/db-setup.ts
Normal file
7
playwright/tests/setups/db-setup.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { test } from './db-test';
|
||||
|
||||
const utils = require('../../global-utils');
|
||||
|
||||
test('DB start', async ({ serviceName }) => {
|
||||
utils.startComposeService(serviceName);
|
||||
});
|
11
playwright/tests/setups/db-teardown.ts
Normal file
11
playwright/tests/setups/db-teardown.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { test } from './db-test';
|
||||
|
||||
const utils = require('../../global-utils');
|
||||
|
||||
utils.loadEnv();
|
||||
|
||||
test('DB teardown ?', async ({ serviceName }) => {
|
||||
if( process.env.PW_KEEP_SERVICE_RUNNNING !== "true" ) {
|
||||
utils.stopComposeService(serviceName);
|
||||
}
|
||||
});
|
9
playwright/tests/setups/db-test.ts
Normal file
9
playwright/tests/setups/db-test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
export type TestOptions = {
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
export const test = base.extend<TestOptions>({
|
||||
serviceName: ['', { option: true }],
|
||||
});
|
77
playwright/tests/setups/orgs.ts
Normal file
77
playwright/tests/setups/orgs.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { expect, type Browser,Page } from '@playwright/test';
|
||||
|
||||
import * as utils from '../../global-utils';
|
||||
|
||||
export async function create(test, page: Page, name: string) {
|
||||
await test.step('Create Org', async () => {
|
||||
await page.locator('a').filter({ hasText: 'Password Manager' }).first().click();
|
||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||
await page.getByRole('link', { name: 'New organisation' }).click();
|
||||
await page.getByLabel('Organisation name (required)').fill(name);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await utils.checkNotification(page, 'Organisation created');
|
||||
});
|
||||
}
|
||||
|
||||
export async function policies(test, page: Page, name: string) {
|
||||
await test.step(`Navigate to ${name} policies`, async () => {
|
||||
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
||||
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
||||
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
|
||||
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
|
||||
await page.getByRole('link', { name: 'Policies' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Policies' })).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
export async function members(test, page: Page, name: string) {
|
||||
await test.step(`Navigate to ${name} members`, async () => {
|
||||
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
||||
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
||||
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
|
||||
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
||||
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
|
||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'All' })).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
export async function invite(test, page: Page, name: string, email: string) {
|
||||
await test.step(`Invite ${email}`, async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Invite member' }).click();
|
||||
await page.getByLabel('Email (required)').fill(email);
|
||||
await page.getByRole('tab', { name: 'Collections' }).click();
|
||||
await page.getByRole('combobox', { name: 'Permission' }).click();
|
||||
await page.getByText('Edit items', { exact: true }).click();
|
||||
await page.getByLabel('Select collections').click();
|
||||
await page.getByText('Default collection').click();
|
||||
await page.getByRole('cell', { name: 'Collection', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await utils.checkNotification(page, 'User(s) invited');
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirm(test, page: Page, name: string, user_email: string) {
|
||||
await test.step(`Confirm ${user_email}`, async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||
await page.getByRole('row').filter({hasText: user_email}).getByLabel('Options').click();
|
||||
await page.getByRole('menuitem', { name: 'Confirm' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Confirm user' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await utils.checkNotification(page, 'confirmed');
|
||||
});
|
||||
}
|
||||
|
||||
export async function revoke(test, page: Page, name: string, user_email: string) {
|
||||
await test.step(`Revoke ${user_email}`, async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||
await page.getByRole('row').filter({hasText: user_email}).getByLabel('Options').click();
|
||||
await page.getByRole('menuitem', { name: 'Revoke access' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Revoke access' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Revoke access' }).click();
|
||||
await utils.checkNotification(page, 'Revoked organisation access');
|
||||
});
|
||||
}
|
18
playwright/tests/setups/sso-setup.ts
Normal file
18
playwright/tests/setups/sso-setup.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
|
||||
const { exec } = require('node:child_process');
|
||||
const utils = require('../../global-utils');
|
||||
|
||||
utils.loadEnv();
|
||||
|
||||
test.beforeAll('Setup', async () => {
|
||||
console.log("Starting Keycloak");
|
||||
exec(`docker compose --profile keycloak --env-file test.env up`);
|
||||
});
|
||||
|
||||
test('Keycloak is up', async ({ page }) => {
|
||||
await utils.waitFor(process.env.SSO_AUTHORITY, page.context().browser());
|
||||
// Dummy authority is created at the end of the setup
|
||||
await utils.waitFor(process.env.DUMMY_AUTHORITY, page.context().browser());
|
||||
console.log(`Keycloak running on: ${process.env.SSO_AUTHORITY}`);
|
||||
});
|
15
playwright/tests/setups/sso-teardown.ts
Normal file
15
playwright/tests/setups/sso-teardown.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, type FullConfig } from '@playwright/test';
|
||||
|
||||
const { execSync } = require('node:child_process');
|
||||
const utils = require('../../global-utils');
|
||||
|
||||
utils.loadEnv();
|
||||
|
||||
test('Keycloak teardown', async () => {
|
||||
if( process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
|
||||
console.log("Keep Keycloak running");
|
||||
} else {
|
||||
console.log("Keycloak stopping");
|
||||
execSync(`docker compose --profile keycloak --env-file test.env stop Keycloak`);
|
||||
}
|
||||
});
|
138
playwright/tests/setups/sso.ts
Normal file
138
playwright/tests/setups/sso.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { expect, type Page, Test } from '@playwright/test';
|
||||
import { type MailBuffer, MailServer } from 'maildev';
|
||||
import * as OTPAuth from "otpauth";
|
||||
|
||||
import * as utils from '../../global-utils';
|
||||
import { retrieveEmailCode } from './2fa';
|
||||
|
||||
/**
|
||||
* If a MailBuffer is passed it will be used and consume the expected emails
|
||||
*/
|
||||
export async function logNewUser(
|
||||
test: Test,
|
||||
page: Page,
|
||||
user: { email: string, name: string, password: string },
|
||||
options: { mailBuffer?: MailBuffer, override?: boolean } = {}
|
||||
) {
|
||||
await test.step(`Create user ${user.name}`, async () => {
|
||||
await page.context().clearCookies();
|
||||
|
||||
await test.step('Landing page', async () => {
|
||||
await utils.cleanLanding(page);
|
||||
|
||||
if( options.override ) {
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
} else {
|
||||
await page.getByLabel(/Email address/).fill(user.email);
|
||||
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('Keycloak login', async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
|
||||
await page.getByLabel(/Username/).fill(user.name);
|
||||
await page.getByLabel('Password', { exact: true }).fill(user.password);
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
});
|
||||
|
||||
await test.step('Create Vault account', async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
|
||||
await page.getByLabel('New master password (required)', { exact: true }).fill(user.password);
|
||||
await page.getByLabel('Confirm new master password (').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
});
|
||||
|
||||
await test.step('Default vault page', async () => {
|
||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
await utils.checkNotification(page, 'Account successfully created!');
|
||||
await utils.checkNotification(page, 'Invitation accepted');
|
||||
|
||||
if( options.mailBuffer ){
|
||||
let mailBuffer = options.mailBuffer;
|
||||
await test.step('Check emails', async () => {
|
||||
await mailBuffer.expect((m) => m.subject === "Welcome");
|
||||
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If a MailBuffer is passed it will be used and consume the expected emails
|
||||
*/
|
||||
export async function logUser(
|
||||
test: Test,
|
||||
page: Page,
|
||||
user: { email: string, password: string },
|
||||
options: {
|
||||
mailBuffer ?: MailBuffer,
|
||||
override?: boolean,
|
||||
totp?: OTPAuth.TOTP,
|
||||
mail2fa?: boolean,
|
||||
} = {}
|
||||
) {
|
||||
let mailBuffer = options.mailBuffer;
|
||||
|
||||
await test.step(`Log user ${user.email}`, async () => {
|
||||
await page.context().clearCookies();
|
||||
|
||||
await test.step('Landing page', async () => {
|
||||
await utils.cleanLanding(page);
|
||||
|
||||
if( options.override ) {
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
} else {
|
||||
await page.getByLabel(/Email address/).fill(user.email);
|
||||
await page.getByRole('button', { name: /Use single sign-on/ }).click();
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('Keycloak login', async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
|
||||
await page.getByLabel(/Username/).fill(user.name);
|
||||
await page.getByLabel('Password', { exact: true }).fill(user.password);
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
});
|
||||
|
||||
if( options.totp || options.mail2fa ){
|
||||
let code;
|
||||
|
||||
await test.step('2FA check', async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
|
||||
|
||||
if( options.totp ) {
|
||||
const totp = options.totp;
|
||||
let timestamp = Date.now(); // Needed to use the next token
|
||||
timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;
|
||||
code = totp.generate({timestamp});
|
||||
} else if( options.mail2fa ){
|
||||
code = await retrieveEmailCode(test, page, mailBuffer);
|
||||
}
|
||||
|
||||
await page.getByLabel(/Verification code/).fill(code);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
});
|
||||
}
|
||||
|
||||
await test.step('Unlock vault', async () => {
|
||||
await expect(page).toHaveTitle('Vaultwarden Web');
|
||||
await expect(page.getByRole('heading', { name: 'Your vault is locked' })).toBeVisible();
|
||||
await page.getByLabel('Master password').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||
});
|
||||
|
||||
await test.step('Default vault page', async () => {
|
||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
if( mailBuffer ){
|
||||
await test.step('Check email', async () => {
|
||||
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
55
playwright/tests/setups/user.ts
Normal file
55
playwright/tests/setups/user.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { expect, type Browser, Page } from '@playwright/test';
|
||||
|
||||
import { type MailBuffer } from 'maildev';
|
||||
|
||||
import * as utils from '../../global-utils';
|
||||
|
||||
export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) {
|
||||
await test.step(`Create user ${user.name}`, async () => {
|
||||
await utils.cleanLanding(page);
|
||||
|
||||
await page.getByRole('link', { name: 'Create account' }).click();
|
||||
|
||||
// Back to Vault create account
|
||||
await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);
|
||||
await page.getByLabel(/Email address/).fill(user.email);
|
||||
await page.getByLabel('Name').fill(user.name);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
// Vault finish Creation
|
||||
await page.getByLabel('New master password (required)', { exact: true }).fill(user.password);
|
||||
await page.getByLabel('Confirm new master password (').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
|
||||
await utils.checkNotification(page, 'Your new account has been created')
|
||||
|
||||
// We are now in the default vault page
|
||||
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
||||
await utils.checkNotification(page, 'You have been logged in!');
|
||||
|
||||
if( mailBuffer ){
|
||||
await mailBuffer.expect((m) => m.subject === "Welcome");
|
||||
await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) {
|
||||
await test.step(`Log user ${user.email}`, async () => {
|
||||
await utils.cleanLanding(page);
|
||||
|
||||
await page.getByLabel(/Email address/).fill(user.email);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
// Unlock page
|
||||
await page.getByLabel('Master password').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Log in with master password' }).click();
|
||||
|
||||
// We are now in the default vault page
|
||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||
|
||||
if( mailBuffer ){
|
||||
await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user