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:
parent
3730355434
commit
9b8cb3f53e
18 changed files with 135 additions and 198 deletions
65
SSO.md
65
SSO.md
|
@ -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).
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"]
|
|
@ -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:-}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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/);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue