1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-07-06 12:35:00 +00:00

Minor improvements

This commit is contained in:
Timshel 2025-05-31 11:59:47 +02:00
parent 3730355434
commit 9b8cb3f53e
18 changed files with 135 additions and 198 deletions

65
SSO.md
View file

@ -12,42 +12,42 @@ This introduces another way to control who can use the vault without having to u
The following configurations are available
- `SSO_ENABLED` : Activate the SSO
- `SSO_ONLY` : disable email+Master password authentication
- `SSO_SIGNUPS_MATCH_EMAIL`: On SSO Signup if a user with a matching email already exists make the association (default `true`)
- `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION`: Allow unknown email verification status (default `false`). Allowing this with `SSO_SIGNUPS_MATCH_EMAIL` open potential account takeover.
- `SSO_AUTHORITY` : the OpenID Connect Discovery endpoint of your SSO
- Should not include the `/.well-known/openid-configuration` part and no trailing `/`
- $SSO_AUTHORITY/.well-known/openid-configuration should return the a json document: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
- `SSO_SCOPES` : Optional, allow to override scopes if needed (default `"email profile"`)
- `SSO_AUTHORIZE_EXTRA_PARAMS` : Optional, allow to add extra parameter to the authorize redirection (default `""`)
- `SSO_PKCE`: Activate PKCE for the Auth Code flow (default `true`).
- `SSO_AUDIENCE_TRUSTED`: Optional, Regex to trust additional audience for the IdToken (`client_id` is always trusted). Use single quote when writing the regex: `'^$'`.
- `SSO_CLIENT_ID` : Client Id
- `SSO_CLIENT_SECRET` : Client Secret
- `SSO_MASTER_PASSWORD_POLICY`: Optional Master password policy (`enforceOnLogin` is not supported).
- `SSO_AUTH_ONLY_NOT_SESSION`: Enable to use SSO only for authentication not session lifecycle
- `SSO_CLIENT_CACHE_EXPIRATION`: Cache calls to the discovery endpoint, duration in seconds, `0` to disable (default `0`);
- `SSO_DEBUG_TOKENS`: Log all tokens (default `false`, `LOG_LEVEL=debug` is required)
- `SSO_ENABLED` : Activate the SSO
- `SSO_ONLY` : disable email+Master password authentication
- `SSO_SIGNUPS_MATCH_EMAIL`: On SSO Signup if a user with a matching email already exists make the association (default `true`)
- `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION`: Allow unknown email verification status (default `false`). Allowing this with `SSO_SIGNUPS_MATCH_EMAIL` open potential account takeover.
- `SSO_AUTHORITY` : the OpenID Connect Discovery endpoint of your SSO
- Should not include the `/.well-known/openid-configuration` part and no trailing `/`
- $SSO_AUTHORITY/.well-known/openid-configuration should return the a json document: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
- `SSO_SCOPES` : Optional, allow to override scopes if needed (default `"email profile"`)
- `SSO_AUTHORIZE_EXTRA_PARAMS` : Optional, allow to add extra parameter to the authorize redirection (default `""`)
- `SSO_PKCE`: Activate PKCE for the Auth Code flow (default `true`).
- `SSO_AUDIENCE_TRUSTED`: Optional, Regex to trust additional audience for the IdToken (`client_id` is always trusted). Use single quote when writing the regex: `'^$'`.
- `SSO_CLIENT_ID` : Client Id
- `SSO_CLIENT_SECRET` : Client Secret
- `SSO_MASTER_PASSWORD_POLICY`: Optional Master password policy (`enforceOnLogin` is not supported).
- `SSO_AUTH_ONLY_NOT_SESSION`: Enable to use SSO only for authentication not session lifecycle
- `SSO_CLIENT_CACHE_EXPIRATION`: Cache calls to the discovery endpoint, duration in seconds, `0` to disable (default `0`);
- `SSO_DEBUG_TOKENS`: Log all tokens for easier debugging (default `false`, `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` need to be set)
The callback url is : `https://your.domain/identity/connect/oidc-signin`
## Account and Email handling
When logging in with SSO an identifier (`{iss}/{sub}` claims from the IdToken) is saved in a separate table (`sso_users`).
This is used to link to the SSO provider identifier without changing the default Vaultwarden user `uuid`. This is needed because:
This is used to link to the SSO provider identifier without changing the default user `uuid`. This is needed because:
- Storing the SSO identifier is important to prevent account takeover due to email change.
- We can't use the identifier as the User uuid since it's way longer (Max 255 chars for the `sub` part, cf [spec](https://openid.net/specs/openid-connect-core-1_0.html#IDToken)).
- We want to be able to associate existing account based on `email` but only when the user logs in for the first time (controlled by `SSO_SIGNUPS_MATCH_EMAIL`).
- We need to be able to associate with existing stub account, such as the one created when inviting a user to an org (association is possible only if the user does not have a private key).
- Storing the SSO identifier is important to prevent account takeover due to email change.
- We can't use the identifier as the User uuid since it's way longer (Max 255 chars for the `sub` part, cf [spec](https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken)).
- We want to be able to associate existing account based on `email` but only when the user logs in for the first time (controlled by `SSO_SIGNUPS_MATCH_EMAIL`).
- We need to be able to associate with existing stub account, such as the one created when inviting a user to an org (association is possible only if the user does not have a private key).
Additionally:
- Signup to Vaultwarden will be blocked if the Provider reports the email as `unverified`.
- Changing the email needs to be done by the user since it requires updating the `key`.
On login if the email returned by the provider is not the one saved in Vaultwarden an email will be sent to the user to ask him to update it.
- If set `SIGNUPS_DOMAINS_WHITELIST` is applied on SSO signup and when attempting to change the email.
- Signup will be blocked if the Provider reports the email as `unverified`.
- Changing the email needs to be done by the user since it requires updating the `key`.
On login if the email returned by the provider is not the one saved an email will be sent to the user to ask him to update it.
- If set `SIGNUPS_DOMAINS_WHITELIST` is applied on SSO signup and when attempting to change the email.
This means that if you ever need to change the provider url or the provider itself; you'll have to first delete the association
then ensure that `SSO_SIGNUPS_MATCH_EMAIL` is activated to allow a new association.
@ -94,7 +94,7 @@ As mentioned in the Google example setting too high of a value has diminishing r
## Keycloak
Default access token lifetime might be only `5min`, set a longer value otherwise it will collide with `VaultWarden` front-end expiration detection which is also set at `5min`.
Default access token lifetime might be only `5min`, set a longer value otherwise it will collide with `Bitwarden` front-end expiration detection which is also set at `5min`.
\
At the realm level
@ -115,11 +115,10 @@ If you want to run a testing instance of Keycloak the Playwright [docker-compose
\
More details on how to use it in [README.md](playwright/README.md#openid-connect-test-setup).
## Auth0
Not working due to the following issue https://github.com/ramosbugs/openidconnect-rs/issues/23 (they appear not to follow the spec).
A feature flag is available to bypass the issue but since it's a compile time feature you will have to patch `Vaultwarden` with something like:
A feature flag is available to bypass the issue but since it's a compile time feature you will have to patch with something like:
```patch
diff --git a/Cargo.toml b/Cargo.toml
@ -148,7 +147,7 @@ Config will look like:
## Authentik
Default access token lifetime might be only `5min`, set a longer value otherwise it will collide with `VaultWarden` front-end expiration detection which is also set at `5min`.
Default access token lifetime might be only `5min`, set a longer value otherwise it will collide with `Bitwarden` front-end expiration detection which is also set at `5min`.
\
To change the tokens expiration go to `Applications / Providers / Edit / Advanced protocol settings`.
@ -208,7 +207,7 @@ Nothing specific should work with just `SSO_AUTHORITY`, `SSO_CLIENT_ID` and `SSO
1. Create an "App registration" in [Entra ID](https://entra.microsoft.com/) following [Identity | Applications | App registrations](https://entra.microsoft.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade/quickStartType//sourceType/Microsoft_AAD_IAM).
2. From the "Overview" of your "App registration", you'll need the "Directory (tenant) ID" for the `SSO_AUTHORITY` variable and the "Application (client) ID" as the `SSO_CLIENT_ID` value.
3. In "Certificates & Secrets" create an "App secret" , you'll need the "Secret Value" for the `SSO_CLIENT_SECRET` variable.
4. In "Authentication" add <https://vaultwarden.example.org/identity/connect/oidc-signin> as "Web Redirect URI".
4. In "Authentication" add <https://warden.example.org/identity/connect/oidc-signin> as "Web Redirect URI".
5. In "API Permissions" make sure you have `profile`, `email` and `offline_access` listed under "API / Permission name" (`offline_access` is required, otherwise no refresh_token is returned, see <https://github.com/MicrosoftDocs/azure-docs/issues/17134>).
Only the v2 endpoint is compliant with the OpenID spec, see <https://github.com/MicrosoftDocs/azure-docs/issues/38427> and <https://github.com/ramosbugs/openidconnect-rs/issues/122>.
@ -270,8 +269,8 @@ Config will look like:
Session lifetime is dependant on refresh token and access token returned after calling your SSO token endpoint (grant type `authorization_code`).
If no refresh token is returned then the session will be limited to the access token lifetime.
Tokens are not persisted in VaultWarden but wrapped in JWT tokens and returned to the application (The `refresh_token` and `access_token` values returned by VW `identity/connect/token` endpoint).
Note that VaultWarden will always return a `refresh_token` for compatibility reasons with the web front and it presence does not indicate that a refresh token was returned by your SSO (But you can decode its value with <https://jwt.io> and then check if the `token` field contain anything).
Tokens are not persisted in the server but wrapped in JWT tokens and returned to the application (The `refresh_token` and `access_token` values returned by VW `identity/connect/token` endpoint).
Note that the server will always return a `refresh_token` for compatibility reasons with the web front and it presence does not indicate that a refresh token was returned by your SSO (But you can decode its value with <https://jwt.io> and then check if the `token` field contain anything).
With a refresh token present, activity in the application will trigger a refresh of the access token when it's close to expiration ([5min](https://github.com/bitwarden/clients/blob/0bcb45ed5caa990abaff735553a5046e85250f24/libs/common/src/auth/services/token.service.ts#L126) in web client).

View file

@ -29,7 +29,7 @@ KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN}
KC_HTTP_HOST=127.0.0.1
KC_HTTP_PORT=8080
# Script parameters (use Keycloak and VaultWarden config too)
# Script parameters (use Keycloak and Vaultwarden config too)
TEST_REALM=test
DUMMY_REALM=dummy
DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
@ -45,8 +45,8 @@ I_REALLY_WANT_VOLATILE_STORAGE=true
SSO_ENABLED=true
SSO_ONLY=false
SSO_CLIENT_ID=VaultWarden
SSO_CLIENT_SECRET=VaultWarden
SSO_CLIENT_ID=warden
SSO_CLIENT_SECRET=warden
SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}
SMTP_HOST=127.0.0.1

View file

@ -143,18 +143,7 @@ You can run just `Keycloak` with `--profile keycloak`:
```bash
> docker compose --profile keycloak --env-file .env up
```
When running with a local VaultWarden and the default `web-vault` you'll need to make the SSO button visible using :
```bash
sed -i 's#a\[routerlink="/sso"\],##' web-vault/app/main.*.css
```
Otherwise you'll need to reveal the SSO login button using the debug console (F12)
```js
document.querySelector('a[routerlink="/sso"]').style.setProperty("display", "inline-block", "important");
```
When running with a local VaultWarden, you can use a front-end build from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).
## Rebuilding the Vaultwarden

View file

@ -1,4 +1,4 @@
FROM playwright_oidc_vaultwarden_prebuilt AS vaultwarden
FROM playwright_oidc_vaultwarden_prebuilt AS prebuilt
FROM node:18-bookworm AS build
@ -8,7 +8,8 @@ ARG COMMIT_HASH
ENV REPO_URL=$REPO_URL
ENV COMMIT_HASH=$COMMIT_HASH
COPY --from=vaultwarden /web-vault /web-vault
COPY --from=prebuilt /web-vault /web-vault
COPY build.sh /build.sh
RUN /build.sh
@ -32,8 +33,8 @@ RUN mkdir /data && \
# and the binary from the "build" stage to the current stage
WORKDIR /
COPY --from=vaultwarden /start.sh .
COPY --from=vaultwarden /vaultwarden .
COPY --from=prebuilt /start.sh .
COPY --from=prebuilt /vaultwarden .
COPY --from=build /web-vault ./web-vault
ENTRYPOINT ["/start.sh"]

View file

@ -15,7 +15,7 @@ services:
image: playwright_oidc_vaultwarden-${ENV:-dev}
network_mode: "host"
build:
context: compose/vaultwarden
context: compose/warden
dockerfile: Dockerfile
args:
REPO_URL: ${PW_WV_REPO_URL:-}

View file

@ -170,7 +170,7 @@ function dbConfig(testInfo: TestInfo){
/**
* All parameters passed in `env` need to be added to the docker-compose.yml
**/
export async function startVaultwarden(browser: Browser, testInfo: TestInfo, env = {}, resetDB: Boolean = true) {
export async function startVault(browser: Browser, testInfo: TestInfo, env = {}, resetDB: Boolean = true) {
if( resetDB ){
switch(testInfo.project.name) {
case "postgres":
@ -195,14 +195,18 @@ export async function startVaultwarden(browser: Browser, testInfo: TestInfo, env
console.log(`Vaultwarden running on: ${process.env.DOMAIN}`);
}
export async function stopVaultwarden() {
console.log(`Vaultwarden stopping`);
execSync(`docker compose --profile playwright --env-file test.env stop Vaultwarden`);
export async function stopVault(force: boolean = false) {
if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`);
} else {
console.log(`Vaultwarden stopping`);
execSync(`docker compose --profile playwright --env-file test.env stop Vaultwarden`);
}
}
export async function restartVaultwarden(page: Page, testInfo: TestInfo, env, resetDB: Boolean = true) {
stopVaultwarden();
return startVaultwarden(page.context().browser(), testInfo, env, resetDB);
export async function restartVault(page: Page, testInfo: TestInfo, env, resetDB: Boolean = true) {
stopVault(true);
return startVault(page.context().browser(), testInfo, env, resetDB);
}
export async function checkNotification(page: Page, hasText: string) {

View file

@ -11,7 +11,7 @@ DOCKER_BUILDKIT=1
# Playwright Config #
#####################
PW_KEEP_SERVICE_RUNNNING=${PW_KEEP_SERVICE_RUNNNING:-false}
VAULTWARDEN_SMTP_FROM=vaultwarden@playwright.test
PW_SMTP_FROM=vaultwarden@playwright.test
#####################
# Maildev Config #
@ -61,8 +61,8 @@ SMTP_PORT=${MAILDEV_SMTP_PORT}
SMTP_FROM_NAME=Vaultwarden
SMTP_TIMEOUT=5
SSO_CLIENT_ID=VaultWarden
SSO_CLIENT_SECRET=VaultWarden
SSO_CLIENT_ID=warden
SSO_CLIENT_SECRET=warden
SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}
SSO_DEBUG_TOKENS=true
@ -70,24 +70,24 @@ SSO_DEBUG_TOKENS=true
# Docker MariaDb container#
###########################
MARIADB_PORT=3307
MARIADB_ROOT_PASSWORD=vaultwarden
MARIADB_USER=vaultwarden
MARIADB_PASSWORD=vaultwarden
MARIADB_DATABASE=vaultwarden
MARIADB_ROOT_PASSWORD=warden
MARIADB_USER=warden
MARIADB_PASSWORD=warden
MARIADB_DATABASE=warden
###########################
# Docker Mysql container#
###########################
MYSQL_PORT=3309
MYSQL_ROOT_PASSWORD=vaultwarden
MYSQL_USER=vaultwarden
MYSQL_PASSWORD=vaultwarden
MYSQL_DATABASE=vaultwarden
MYSQL_ROOT_PASSWORD=warden
MYSQL_USER=warden
MYSQL_PASSWORD=warden
MYSQL_DATABASE=warden
############################
# Docker Postgres container#
############################
POSTGRES_PORT=5433
POSTGRES_USER=vaultwarden
POSTGRES_PASSWORD=vaultwarden
POSTGRES_DB=vaultwarden
POSTGRES_USER=warden
POSTGRES_PASSWORD=warden
POSTGRES_DB=warden

View file

@ -1,16 +1,16 @@
import { test, expect, type TestInfo } from '@playwright/test';
import * as utils from "../global-utils";
import { createAccount, logUser } from './setups/user';
import { createAccount } from './setups/user';
let users = utils.loadEnv();
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVaultwarden(browser, testInfo);
await utils.startVault(browser, testInfo);
});
test.afterAll('Teardown', async ({}) => {
utils.stopVaultwarden();
utils.stopVault();
});
test('Create', async ({ page }) => {

View file

@ -17,14 +17,14 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await mailserver.listen();
await utils.startVaultwarden(browser, testInfo, {
await utils.startVault(browser, testInfo, {
SMTP_HOST: process.env.MAILDEV_HOST,
SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM,
SMTP_FROM: process.env.PW_SMTP_FROM,
});
});
test.afterAll('Teardown', async ({}) => {
utils.stopVaultwarden();
utils.stopVault();
if( mailserver ){
await mailserver.close();
}
@ -32,7 +32,9 @@ test.afterAll('Teardown', async ({}) => {
test('Account creation', async ({ page }) => {
const mailBuffer = mailserver.buffer(users.user1.email);
await createAccount(test, page, users.user1, mailBuffer);
mailBuffer.close();
});
@ -49,7 +51,7 @@ test('Login', async ({ context, page }) => {
await utils.checkNotification(page, 'Check your email inbox for a verification link');
const verify = await mailBuffer.next((m) => m.subject === "Verify Your Email");
expect(verify.from[0]?.address).toBe(process.env.VAULTWARDEN_SMTP_FROM);
expect(verify.from[0]?.address).toBe(process.env.PW_SMTP_FROM);
const page2 = await context.newPage();
await page2.setContent(verify.html);
@ -63,7 +65,7 @@ test('Login', async ({ context, page }) => {
mailBuffer.close();
});
test('Activaite 2fa', async ({ context, page }) => {
test('Activate 2fa', async ({ page }) => {
const emails = mailserver.buffer(users.user1.email);
await logUser(test, page, users.user1);
@ -73,7 +75,7 @@ test('Activaite 2fa', async ({ context, page }) => {
emails.close();
});
test('2fa', async ({ context, page }) => {
test('2fa', async ({ page }) => {
const emails = mailserver.buffer(users.user1.email);
await test.step('login', async () => {
@ -84,16 +86,12 @@ test('2fa', async ({ context, page }) => {
await page.getByLabel('Master password').fill(users.user1.password);
await page.getByRole('button', { name: 'Log in with master password' }).click();
const codeMail = await emails.next((mail) => mail.subject === "Vaultwarden Login Verification Code");
const page2 = await context.newPage();
await page2.setContent(codeMail.html);
const code = await page2.getByTestId("2fa").innerText();
await page2.close();
await page.getByLabel('Verification code').fill(code);
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
const code = await retrieveEmailCode(test, page, emails);
await page.getByLabel(/Verification code/).fill(code);
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page).toHaveTitle(/Vaultwarden Web/);
await expect(page).toHaveTitle(/Vaults/);
})
await disableEmail(test, page, users.user1);

View file

@ -9,11 +9,11 @@ let users = utils.loadEnv();
let totp;
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVaultwarden(browser, testInfo, {});
await utils.startVault(browser, testInfo, {});
});
test.afterAll('Teardown', async ({}, testInfo: TestInfo) => {
utils.stopVaultwarden(testInfo);
test.afterAll('Teardown', async ({}) => {
utils.stopVault();
});
test('Account creation', async ({ page }) => {
@ -24,7 +24,7 @@ test('Master password login', async ({ page }) => {
await logUser(test, page, users.user1);
});
test('Authenticator 2fa', async ({ context, page }) => {
test('Authenticator 2fa', async ({ page }) => {
await logUser(test, page, users.user1);
let totp = await activateTOTP(test, page, users.user1);
@ -36,7 +36,7 @@ test('Authenticator 2fa', async ({ context, page }) => {
});
await test.step('login', async () => {
let timestamp = Date.now(); // Need to use the next token
let timestamp = Date.now(); // Needed 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);
@ -44,7 +44,8 @@ test('Authenticator 2fa', async ({ context, page }) => {
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 expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
await page.getByLabel(/Verification code/).fill(totp.generate({timestamp}));
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page).toHaveTitle(/Vaultwarden Web/);

View file

@ -17,9 +17,9 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await mailServer.listen();
await utils.startVaultwarden(browser, testInfo, {
await utils.startVault(browser, testInfo, {
SMTP_HOST: process.env.MAILDEV_HOST,
SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM,
SMTP_FROM: process.env.PW_SMTP_FROM,
});
mail1Buffer = mailServer.buffer(users.user1.email);
@ -28,7 +28,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
});
test.afterAll('Teardown', async ({}, testInfo: TestInfo) => {
utils.stopVaultwarden(testInfo);
utils.stopVault(testInfo);
[mail1Buffer, mail2Buffer, mail3Buffer, mailServer].map((m) => m?.close());
});
@ -110,6 +110,6 @@ test('Confirm invited user', async ({ page }) => {
test('Organization is visible', async ({ page }) => {
await logUser(test, page, users.user2, mail2Buffer);
await page.getByLabel('vault: Test').click();
await page.getByRole('button', { name: 'vault: Test', exact: true }).click();
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
});

View file

@ -8,11 +8,11 @@ import { createAccount, logUser } from './setups/user';
let users = utils.loadEnv();
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVaultwarden(browser, testInfo);
await utils.startVault(browser, testInfo);
});
test.afterAll('Teardown', async ({}) => {
utils.stopVaultwarden();
utils.stopVault();
});
test('Invite', async ({ page }) => {

View file

@ -1,4 +1,5 @@
import { expect, type Browser,Page } from '@playwright/test';
import { expect, type Browser, Page } from '@playwright/test';
import { type MailBuffer } from 'maildev';
import * as utils from '../../global-utils';
@ -28,6 +29,7 @@ export async function createAccount(test, page: Page, user: { email: string, nam
if( mailBuffer ){
await expect(mailBuffer.next((m) => m.subject === "Welcome")).resolves.toBeDefined();
await expect(mailBuffer.next((m) => m.subject === "New Device Logged In From Firefox")).resolves.toBeDefined();
}
});
}
@ -47,7 +49,7 @@ export async function logUser(test, page: Page, user: { email: string, password:
await expect(page).toHaveTitle(/Vaultwarden Web/);
if( mailBuffer ){
await expect(mailBuffer.next((m) => m.subject === 'New Device Logged In From Firefox')).resolves.toBeDefined();
await expect(mailBuffer.next((m) => m.subject === "New Device Logged In From Firefox")).resolves.toBeDefined();
}
});
}

View file

@ -17,16 +17,16 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await mailserver.listen();
await utils.startVaultwarden(browser, testInfo, {
await utils.startVault(browser, testInfo, {
SSO_ENABLED: true,
SSO_ONLY: false,
SMTP_HOST: process.env.MAILDEV_HOST,
SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM,
SMTP_FROM: process.env.PW_SMTP_FROM,
});
});
test.afterAll('Teardown', async ({}) => {
utils.stopVaultwarden();
utils.stopVault();
if( mailserver ){
await mailserver.close();
}

View file

@ -7,14 +7,14 @@ import * as utils from "../global-utils";
let users = utils.loadEnv();
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVaultwarden(browser, testInfo, {
await utils.startVault(browser, testInfo, {
SSO_ENABLED: true,
SSO_ONLY: false
});
});
test.afterAll('Teardown', async ({}) => {
utils.stopVaultwarden();
utils.stopVault();
});
test('Account creation using SSO', async ({ page }) => {
@ -51,13 +51,14 @@ test('SSO login with TOTP 2fa', async ({ page }) => {
});
test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) => {
await utils.restartVaultwarden(page, testInfo, {
await utils.restartVault(page, testInfo, {
SSO_ENABLED: true,
SSO_ONLY: true
}, false);
// Landing page
await page.goto('/');
await page.getByLabel(/Email address/).fill(users.user1.email);
// Check that SSO login is available
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);
@ -75,7 +76,7 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) =
test('No SSO login', async ({ page }, testInfo: TestInfo) => {
await utils.restartVaultwarden(page, testInfo, {
await utils.restartVault(page, testInfo, {
SSO_ENABLED: false
}, false);
@ -84,5 +85,10 @@ test('No SSO login', async ({ page }, testInfo: TestInfo) => {
await page.getByLabel(/Email address/).fill(users.user1.email);
// No SSO button (rely on a correct selector checked in previous test)
await page.getByLabel('Master password');
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);
// Can continue to Master password
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('button', { name: /Log in with master password/ })).toHaveCount(1);
});

View file

@ -2,6 +2,7 @@ import { test, expect, type TestInfo } from '@playwright/test';
import { MailDev } from 'maildev';
import * as utils from "../global-utils";
import * as orgs from './setups/orgs';
import { logNewUser, logUser } from './setups/sso';
let users = utils.loadEnv();
@ -16,9 +17,9 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await mailServer.listen();
await utils.startVaultwarden(browser, testInfo, {
await utils.startVault(browser, testInfo, {
SMTP_HOST: process.env.MAILDEV_HOST,
SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM,
SMTP_FROM: process.env.PW_SMTP_FROM,
SSO_ENABLED: true,
SSO_ONLY: true,
});
@ -29,7 +30,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
});
test.afterAll('Teardown', async ({}) => {
utils.stopVaultwarden();
utils.stopVault();
[mail1Buffer, mail2Buffer, mail3Buffer, mailServer].map((m) => m?.close());
});
@ -40,39 +41,15 @@ test('Create user3', async ({ page }) => {
test('Invite users', async ({ page }) => {
await logNewUser(test, page, users.user1, { mailBuffer: mail1Buffer });
await test.step('Create Org', async () => {
await page.getByRole('link', { name: 'New organisation' }).click();
await page.getByLabel('Organisation name (required)').fill('Test');
await page.getByRole('button', { name: 'Submit' }).click();
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
});
await test.step('Invite user2', async () => {
await page.getByRole('button', { name: 'Invite member' }).click();
await page.getByLabel('Email (required)').fill(users.user2.email);
await page.getByRole('tab', { name: 'Collections' }).click();
await page.getByLabel('Permission').selectOption('edit');
await page.getByLabel('Select collections').click();
await page.getByLabel('Options list').getByText('Default collection').click();
await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'User(s) invited');
});
await test.step('Invite user3', async () => {
await page.getByRole('button', { name: 'Invite member' }).click();
await page.getByLabel('Email (required)').fill(users.user3.email);
await page.getByRole('tab', { name: 'Collections' }).click();
await page.getByLabel('Permission').selectOption('edit');
await page.getByLabel('Select collections').click();
await page.getByLabel('Options list').getByText('Default collection').click();
await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'User(s) invited');
});
await orgs.create(test, page, '/Test');
await orgs.members(test, page, '/Test');
await orgs.invite(test, page, '/Test', users.user2.email);
await orgs.invite(test, page, '/Test', users.user3.email);
});
test('invited with new account', async ({ page }) => {
const link = await test.step('Extract email link', async () => {
const invited = await mail2Buffer.next((m) => m.subject === "Join Test");
const invited = await mail2Buffer.next((m) => m.subject === "Join /Test");
await page.setContent(invited.html);
return await page.getByTestId("invite").getAttribute("href");
});
@ -104,13 +81,13 @@ test('invited with new account', async ({ page }) => {
await test.step('Check mails', async () => {
await expect(mail2Buffer.next((m) => m.subject.includes("New Device Logged"))).resolves.toBeDefined();
await expect(mail1Buffer.next((m) => m.subject === "Invitation to Test accepted")).resolves.toBeDefined();
await expect(mail1Buffer.next((m) => m.subject === "Invitation to /Test accepted")).resolves.toBeDefined();
});
});
test('invited with existing account', async ({ page }) => {
const link = await test.step('Extract email link', async () => {
const invited = await mail3Buffer.next((m) => m.subject === "Join Test");
const invited = await mail3Buffer.next((m) => m.subject === "Join /Test");
await page.setContent(invited.html);
return await page.getByTestId("invite").getAttribute("href");
});
@ -139,6 +116,6 @@ test('invited with existing account', async ({ page }) => {
await test.step('Check mails', async () => {
await expect(mail3Buffer.next((m) => m.subject.includes("New Device Logged"))).resolves.toBeDefined();
await expect(mail1Buffer.next((m) => m.subject === "Invitation to Test accepted")).resolves.toBeDefined();
await expect(mail1Buffer.next((m) => m.subject === "Invitation to /Test accepted")).resolves.toBeDefined();
});
});

View file

@ -2,19 +2,20 @@ import { test, expect, type TestInfo } from '@playwright/test';
import { MailDev } from 'maildev';
import * as utils from "../global-utils";
import * as orgs from './setups/orgs';
import { logNewUser, logUser } from './setups/sso';
let users = utils.loadEnv();
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVaultwarden(browser, testInfo, {
await utils.startVault(browser, testInfo, {
SSO_ENABLED: true,
SSO_ONLY: true,
});
});
test.afterAll('Teardown', async ({}) => {
utils.stopVaultwarden();
utils.stopVault();
});
test('Create user3', async ({ page }) => {
@ -24,43 +25,11 @@ test('Create user3', async ({ page }) => {
test('Invite users', async ({ page }) => {
await logNewUser(test, page, users.user1);
await test.step('Create Org', async () => {
await page.getByRole('link', { name: 'New organisation' }).click();
await page.getByLabel('Organisation name (required)').fill('Test');
await page.getByRole('button', { name: 'Submit' }).click();
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
});
await test.step('Invite user2', async () => {
await page.getByRole('button', { name: 'Invite member' }).click();
await page.getByLabel('Email (required)').fill(users.user2.email);
await page.getByRole('tab', { name: 'Collections' }).click();
await page.getByLabel('Permission').selectOption('edit');
await page.getByLabel('Select collections').click();
await page.getByLabel('Options list').getByText('Default collection').click();
await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'User(s) invited');
await expect(page.getByRole('row', { name: users.user2.email })).toHaveText(/Invited/);
});
await test.step('Invite user3', async () => {
await page.getByRole('button', { name: 'Invite member' }).click();
await page.getByLabel('Email (required)').fill(users.user3.email);
await page.getByRole('tab', { name: 'Collections' }).click();
await page.getByLabel('Permission').selectOption('edit');
await page.getByLabel('Select collections').click();
await page.getByLabel('Options list').getByText('Default collection').click();
await page.getByRole('button', { name: 'Save' }).click();
await utils.checkNotification(page, 'User(s) invited');
await expect(page.getByRole('row', { name: users.user3.name })).toHaveText(/Needs confirmation/);
});
await test.step('Confirm existing user3', async () => {
await page.getByRole('row', { name: users.user3.name }).getByLabel('Options').click();
await page.getByRole('menuitem', { name: 'Confirm' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await utils.checkNotification(page, 'confirmed');
});
await orgs.create(test, page, '/Test');
await orgs.members(test, page, '/Test');
await orgs.invite(test, page, '/Test', users.user2.email);
await orgs.invite(test, page, '/Test', users.user3.email);
await orgs.confirm(test, page, '/Test', users.user3.email);
});
test('Create invited account', async ({ page }) => {
@ -69,22 +38,13 @@ test('Create invited account', async ({ page }) => {
test('Confirm invited user', async ({ page }) => {
await logUser(test, page, users.user1);
await page.getByLabel('Switch products').click();
await page.getByRole('link', { name: ' Admin Console' }).click();
await page.getByRole('link', { name: 'Members' }).click();
await orgs.members(test, page, '/Test');
await expect(page.getByRole('row', { name: users.user2.name })).toHaveText(/Needs confirmation/);
await test.step('Confirm user2', async () => {
await page.getByRole('row', { name: users.user2.name }).getByLabel('Options').click();
await page.getByRole('menuitem', { name: 'Confirm' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await utils.checkNotification(page, 'confirmed');
});
await orgs.confirm(test, page, '/Test', users.user2.email);
});
test('Organization is visible', async ({ page }) => {
await logUser(test, page, users.user2);
await page.getByLabel('vault: Test').click();
await page.getByLabel('vault: /Test').click();
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
});