Add a 23-step single-session lifecycle test covering every code path a
real PRF-passkey user exercises:
register → enrol passkey #1 → enrol passkey #2 on a second virtual
authenticator → log in with passkey → lock + unlock with passkey →
register a second-device context + "Log in with device" approval flow
→ enrol WebAuthn-2FA + TOTP-2FA → log in with passkey (server skips
2FA on webauthn grant) → log in with MP + WebAuthn-2FA → lock + unlock
→ remove passkey #1 → bump KDF iterations (auto-logout) → re-login
with WebAuthn-2FA → rotate account encryption keys (auto-logout) →
re-login with MP + TOTP-2FA → lock + unlock with passkey #2 → remove
passkey #2 → log in with WebAuthn-2FA (post-login sync refreshes the
client cache so the lock-screen assertion sees the credential-free
state) → unlock with MP → disable both 2FA providers → log in with MP
alone.
A sibling `account-lifecycle-sso` project runs the same 23-step
lifecycle under `SSO_ENABLED=true`. Login flows that previously typed
MP at the prompt go through Keycloak + the bundled "Unlock vault"
MP-decrypt step instead, exercising the SSO + WebAuthn-2FA + PRF
passkey composition the original MP-only project couldn't cover. The
body of the lifecycle is shared between both projects via mode-
dispatch (`modeOps(sso)`); CDP virtual-authenticator wrangling, passkey
enrol/remove, lock/unlock, KDF + MP rotation, and the second-device
auth-request flow are extracted into
`tests/setups/account_lifecycle_helpers.ts`.
Notable wire-shape coverage:
* `userDecryption.webAuthnPrfOptions` is populated only after PRF
enrolment and emptied after passkey removal.
* Rotation re-wraps each PRF credential's stored
encryptedUserKey/encryptedPrivateKey; passkey #2 still unlocks the
rotated user key.
* KDF change auto-logs out via security-stamp rotation.
* "Log in with device" is gated on `isKnownDevice` in the bundled web
vault — the test asserts the affordance is absent on a fresh
second-device context and surfaces after that context's first MP
login.
Reuses `logUser`/`submitTwoFactor` from `setups/`; the only spec-local
helpers are CDP-specific (virtual-authenticator creation,
`withAuthenticatorDisabled` callback wrapper) or test-local
expectations (`expectLockScreenButtons`, `expectPostEmailPageNoPasskey`,
etc.).
Supporting changes for SSO mode:
- `enterEmailOnLoginPage`: SSO branch fills `.vw-email-sso` and clicks
"Other" to reveal the MP-continue flow — the standard email-label
selector matches the SCSS-hidden `.vw-email-continue` input under
`SSO_ENABLED=true`.
- `sso.ts#logUser`: accepts a separate `kcPassword` for cases where vault
MP and the IdP credential diverge (post-MP-rotation); accepts either
/#/lock or /#/vault after 2FA so PRF auto-unlock via the lock screen's
`promptBiometric=true` redirect is tolerated; uses
`name: 'Unlock', exact: true` to disambiguate from the
"Unlock with passkey" affordance when PRF is enrolled. The
"Join organi[sz]ation" heading match is locale-tolerant (en vs en_GB).
- `2fa.ts#submitTwoFactor`: post-2FA URL waiter accepts /#/lock too;
TOTP submission tracks its own `last_used` time-step and waits for
the next period boundary when a repeat would land on a consumed
step, so consecutive TOTP submissions in the same period don't trip
vw's `last_used > current` rejection.
- `2fa.ts#ensure2FAProvider`: 5s probe (was 1s) for the default
provider's input before falling through to the picker — under SSO
mode the extra Keycloak round-trip can delay the connector iframe
mount enough to race the switcher.
- `global-utils.ts#cleanLanding`: swallow "navigation interrupted" /
`net::ERR_ABORTED` from `page.goto('/')` — the bundled web vault's
`/` → `/#/login` hash-route redirect occasionally fires while the
initial nav is still resolving under docker's slower I/O.
- `global-utils.ts#startVault`/`dbConfig`: register
`account-lifecycle-sso` as a sqlite-backed project.
- `user.ts#logUser`: accepts `kcPassword` for option-shape parity with
`sso.ts#logUser`. Ignored in MP-only mode.
- `global-setup.ts`: short-circuits the docker-compose build when
`PW_USE_EXTERNAL_VAULT=1` — host-iteration runs against a cargo-run
vw don't need the multi-minute release rebuild.
In SSO mode, login-with-passkey is left as MP-mode-only coverage (the
SSO project skips the two wire-shape probes for the same reason); the
two smaller tests check the same server endpoint shape and are mode-
invariant. The SSO lifecycle uses TOTP-2FA with
`withAuthenticatorDisabled` at the MP-fresh-required logins so the
lock screen waits for manual MP entry instead of auto-firing PRF
unlock.
Plumbing changes that come with this spec:
* `playwright.config.ts` threads
`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` into the `account-lifecycle`
project's `launchOptions.executablePath` so the spec can run
locally against a host-running Vaultwarden on systems where
`npx playwright install chromium` is unsupported.
* `compose/playwright/Dockerfile` installs Chromium alongside Firefox
so the docker harness can run the `account-lifecycle` project too.
* `account_lifecycle.spec.ts` honors `PW_USE_EXTERNAL_VAULT=1` to skip
the docker startVault/stopVault hooks (host-mode iteration only; CI
leaves it unset).
Run requires `LOGIN_RATELIMIT_MAX_BURST` raised above the 10/60s
default — multiple `connect/token` POSTs during the auth-request
approval + back-to-back 2FA cycles exhaust the limit otherwise.
Runtime: ~1.5min MP-mode, ~2min SSO-mode in docker; both well under
the 180s test budget.
`submitTwoFactor` previously threw `Not Implemented` for `kind: 'fido2'`.
Implement it: the bundled web vault's connector iframe on /#/2fa
auto-fires `navigator.credentials.get()` as soon as it mounts and the
page transitions to /vault on its own, so the helper just waits for that
URL transition (caller is responsible for keeping a virtual authenticator
attached with auto-presence enabled).
Add `ensure2FAProvider`: when the user has multiple 2FA providers
enrolled and the current page is showing a different provider's UI than
the requested one, click "Select another method" → the labeled row for
the requested kind. The label table is fixed per `TwoFactor.kind`
(`/Authenticator app/i` for `totp`, `/Email/i` for `mail2fa`,
`/Passkey|FIDO2/i` for `fido2`), so callers don't have to know about it.
No-op when the requested kind's input is already visible (single-
provider case, or it was already the default).
Pre-MP setup required to keep the page-default provider from auto-firing
(e.g. CDP `setAutomaticPresenceSimulation(false)` for the WebAuthn-2FA
default) remains the caller's responsibility — the CDP authenticator
handle lives in the test spec, not in the shared 2FA helper.
Also moves the post-submit `expect(page).toHaveURL(/vault/)` assertion
inside `submitTwoFactor` so callers don't have to repeat it.
The bundled web vault gates its lock-screen "Unlock with passkey"
affordance on the `pm-2035-passkey-unlock` feature flag in
`/api/config`'s `featureStates`. Without it,
`WebAuthnPrfUnlockService.isPrfUnlockAvailable` short-circuits to
`false` and the button never renders even for users with a PRF-enabled
passkey enrolled.
Vaultwarden supports PRF passkey unlock end-to-end (the
`userDecryption.webAuthnPrfOptions` blob in `/api/sync` feeds the
client-side unwrap), so the flag must be advertised as enabled.
Extracts `build_feature_states` from the `/api/config` handler so the
feature-state assembly is unit-testable without `CONFIG` initialisation,
and pins both the new flag and the existing
`pm-19148-innovation-archive` companion via tests in `src/api/core/mod.rs`
+ a `/api/config` Playwright probe in `passkey.spec.ts`.
`is_webauthn_2fa_supported()` was previously checked only at
`POST /two-factor/get-webauthn`. The three sibling 2FA WebAuthn HTTP
routes — `generate_webauthn_challenge`, `activate_webauthn` (and its
`PUT` alias), and `delete_webauthn` — are user-facing Bearer-authed
endpoints that any client can hit directly. A client that skips the
listing endpoint reached the unguarded `WEBAUTHN.start_passkey_registration`
/ `WEBAUTHN.finish_passkey_registration` calls.
Concretely: under a misconfigured `DOMAIN` (IP literal, hostless URL —
anything where `Url::domain()` returns `None`), the `WEBAUTHN` `LazyLock`
panics on first access from inside `WebauthnBuilder::new("", ..)`,
poisoning the lock so every subsequent WebAuthn touch in the process
panics too. The listing endpoint's gate avoids this on the legitimate
flow but leaves the same panic-cascade reachable via any direct call
to a sibling endpoint.
Apply the same guard at the two endpoints that access `WEBAUTHN`:
- `generate_webauthn_challenge` (calls `WEBAUTHN.start_passkey_registration`)
- `activate_webauthn` (calls `WEBAUTHN.finish_passkey_registration`;
also covers `activate_webauthn_put` which is a thin wrapper)
`delete_webauthn` doesn't touch `WEBAUTHN` (pure DB work over the
`TwoFactor` row), so it doesn't need the guard.
The check is a pure function of `CONFIG.domain()`; the response is
identical to the listing endpoint's, so well-configured deployments
see no behaviour change.
Adds an end-to-end check that registering a PRF-enabled login passkey
populates `userDecryption.webAuthnPrfOptions` in /api/sync — the wire-level
prerequisite for the web vault's lock-screen "Unlock with passkey" option.
Two tests, complementary:
- PRF enrolment (`useForEncryption` checked) yields a non-empty array in
/sync, with the wrapped-key blobs the client uses to derive the user key
after the PRF assertion.
- Enrolment without PRF (`useForEncryption` unchecked) leaves the array
empty, pinning the emission filter's other branch.
Drives the real "Turn on Log in with passkey" UI flow under Settings →
Security → Master password against the bundled web vault, satisfying the
WebAuthn credential creation step with a Chromium CDP virtual authenticator.
The post-enrolment /sync call sniffs the bearer token from a live SPA
request rather than reaching into IndexedDB, because the vault aggressively
caches sync state and won't re-fetch on demand.
Runs as a dedicated `account-lifecycle` project in `playwright.config.ts`
(Chromium, `en` locale, SQLite-volatile via `utils.startVault`). The four
DB projects exclude the spec via `testIgnore`, since the rest of the suite
runs Firefox and the CDP virtual-authenticator with the `hmac-secret` PRF
extension is Chromium-only.
Why this file isn't in `passkey.spec.ts`:
- The "Log in with passkey" assertion ceremony itself runs inside a
same-origin `/webauthn-connector.html` iframe; current Chromium does not
satisfy navigator.credentials calls inside that iframe via CDP-injected
virtual authenticators. The enrolment step (which runs WebAuthn in the
main frame via a bit-dialog) IS reachable, and that's exactly the step
that populates webAuthnPrfOptions.
Run:
npx playwright test --project=account-lifecycle
Verified against bundled web-vault v2026.4.1: 2/2 passed end-to-end via the
docker harness.
Upstream's `IdentityTokenResponse.UserDecryptionOptions` model carries a
**singular** `WebAuthnPrfOption`, populated solely from
`UserDecryptionOptionsBuilder.WithWebAuthnLoginCredential` after a
successful passkey assertion. Upstream's `SyncResponseModel.UserDecryption`
model carries the **plural** `WebAuthnPrfOptions` array, populated for
every PRF-enabled credential the user owns. The Bitwarden client reads
each at a different point in the flow:
- Singular drives the immediate post-passkey-login vault decryption
(the client combines it with the PRF secret from the assertion it
just performed).
- Plural drives the lock-screen "Unlock with passkey" option (read
from disk state populated at sync time).
Vaultwarden previously emitted only the singular on the webauthn-grant
response and nothing on /sync, so the lock-screen option never appeared
even when the user had a PRF-enabled credential.
This commit:
- Adds `webAuthnPrfOptions` (plural array) to `/sync` via a new
`build_webauthn_prf_options(&[WebAuthnCredential])` helper.
- Extracts the singular emission into `build_webauthn_login_prf_option`
and a wrapper `build_webauthn_login_response` that applies it to the
output of `authenticated_response`. The wrapper makes the call site
unit-testable: a regression that removes the call site trips the
dead-code lint at build time (the helper's only caller).
- Pins both helpers and the response-augmentation wrapper with unit
tests covering shape (field names + values) and behaviour
(idempotency, no-op for non-Enabled credentials, untouched payload
for unsupported credentials).
- Adds a playwright integration test that pins the wire-level response
shapes for password-grant /connect/token and /sync against the
upstream contract.
The login responses for `password` and `client_credentials` grants no
longer emit any PRF field (matching upstream — the singular is only
populated on webauthn grant, and the plural doesn't exist on that
response model at all).
`passkey.spec.ts` exercises the unauthenticated and authorization-required
surface that doesn't need a virtual authenticator:
- `GET /identity/accounts/webauthn/assertion-options` returns the documented
shape (`Content-Type: application/json`, `options` + `userVerification`
+ a non-empty `token`). The token format is intentionally not pinned:
Vaultwarden mints a UUID, upstream Bitwarden mints a
`DataProtectorTokenable`; both are opaque from the client's view.
- Five back-to-back calls return five distinct tokens AND five distinct
challenges — a refactor that re-used either would let an attacker replay.
- The `grant_type=webauthn` token endpoint returns a generic auth-failed
message for an unknown token, a malformed deviceresponse, and a
structurally-valid but unsignable assertion. The regex accepts both
Vaultwarden's "Passkey authentication failed." and upstream Bitwarden's
"Invalid credential." — the security contract is the byte-equality
between failure branches (oracle defense), not the surface text.
- Every webauthn-management endpoint (`GET /api/webauthn`, attestation /
assertion options, finish, update, delete) rejects anonymous callers
AND callers with a garbage Bearer with 401.
- Missing-required-field requests to the webauthn grant are rejected
before the handler body runs (token / deviceresponse / client_id /
scope). The specific rejection text differs between projects so we
only assert that the response is an error.
- The web vault renders the "Log in with passkey" entry point.
Security-gate coverage covers forged user-handle attempts against
disabled and unverified accounts plus the SSO_ONLY webauthn grant gate.
The forged-handle cases create real target users but submit
intentionally unsignable assertions, asserting the response stays
byte-equal to the unknown-user baseline before WebAuthn verification
succeeds. Adds the docker-compose environment passthrough needed for
per-describe vault restarts with SIGNUPS_VERIFY and SSO_ONLY test
configs.
README: document the Playwright image's bake-in behavior — the
Dockerfile copies `tests/` in at build time, so local edits to
`*.spec.ts` are not picked up by `docker compose run Playwright`
until the image is rebuilt. Verified empirically: an in-place rename
of a `test('…')` title is invisible to `run` until `build Playwright`
is invoked, and absolute paths through the mounted `..:/project`
volume don't override Playwright's config-derived `testDir`. Add a
short note next to the existing "force a rebuild" command.
The spec is Firefox-compatible and runs unmodified under the existing
playwright project matrix.
Add a credential_id_hash column containing sha256-hex of the raw WebAuthn credential ID and enforce it with a unique index across all three backends.
Check credential_id_hash before saving to return a clean duplicate-passkey error in the common case, and map database unique violations to the same error to close the TOCTOU window.
Keep PRF keyset/status unit coverage exhaustive so a refactor cannot accidentally report partial PRF state as enabled.
Consume passkey challenges before parsing or user lookup so every submission carrying a valid token is single-use, then delay user-scoped event attribution and account-state checks until the assertion is cryptographically bound to a registered credential.
Return the generic passkey auth failure for malformed assertions, missing or corrupt challenges, unverified email state, and failed WebAuthn verification; do not send verification-reminder email from this unauthenticated flow.
Add WebAuthn DOMAIN compatibility gates, stricter PRF option gating, key-rotation race checks, cascade deletes, login rate limits on authenticated passkey endpoints, and typed credential IDs on delete.
Re-prompt master password or OTP at POST /webauthn, matching the sibling 2FA WebAuthn activation path.
Bind registration and PRF-update ceremonies to random challenge tokens, consume saved challenge rows atomically, and add the authenticated assertion-options/update endpoints for completing PRF unlock setup.
Persist complete PRF unlock keysets with a dedicated column-scoped update helper.
Mirror password-login 2FA policy handling for passkey login: enforce RequireTwoFactor revocation when no providers exist, and reject accounts whose TwoFactor rows are all disabled or unusable.
Replace the per-arm `if sso_enabled && sso_only` guard in `login()` with
a `check_sso_only(grant_type)` helper called once before the grant
dispatch. Mirrors upstream Bitwarden's `SsoRequestValidator`: under
SSO_ONLY every grant is rejected unless explicitly whitelisted
(`authorization_code`, `client_credentials`, `refresh_token`).
A future grant cannot silently bypass SSO — every new `grant_type` must
be added to the explicit whitelist to be allowed under SSO_ONLY.
- Wire key rotation to re-encrypt each passkey's PRF keys, with a superset check in validate_keydata so a passkey can't be left stale.
- Persist signature-counter updates and rotated PRF keys with column-scoped writes, avoiding a broad full-row credential update.
- Compute prfStatus as the full WebAuthnPrfStatus instead of a 1/0 placeholder.
- Move the login challenge from an in-memory cache to a DB-backed table with a scheduled cleanup job.
- Use webauthn-rs's discoverable-credential API instead of the JSON state-injection workaround.
- Make challenge consumption single-use, rate-limit assertion-options, and return a single generic auth-failure message.
- Honor SSO_ONLY at every passkey entry point: login grant, enrollment, refresh, and the unauthenticated assertion-options challenge.
- Migrations: real MySQL foreign key and indexes.
- Add prfStatus unit tests; codebase-consistency pass.
2FA challenge submission was inlined across the suite: `login.spec.ts`,
`login.smtp.spec.ts` and `setups/sso.ts:logUser` each asserted the
"Verify your Identity" heading, generated/retrieved the factor-specific
verification code, then clicked Continue. The only piece that varied was
the source of the code (TOTP generator vs. email-OTP retrieved from
maildev). When the web vault changes the heading copy, the code-input
label, or the Continue-button name, every duplicate has to be hunted
down separately.
Centralises the challenge flow in `setups/2fa.ts` behind a `TwoFactor`
discriminated union and a `submitTwoFactor` dispatcher:
type TwoFactor =
| { kind: 'totp', totp: OTPAuth.TOTP }
| { kind: 'mail2fa', mailBuffer: MailBuffer }
| { kind: 'fido2' };
Each variant carries exactly the state it needs. `submitTwoFactor`
asserts the heading then `switch`es on `kind`: TOTP fills the
next-period-boundary code (avoiding period-boundary expiry races near a
30-second tick) and mail2fa retrieves from the buffer; both then click
Continue. The `fido2` variant is declared so the union covers every
2FA provider the bundled web vault exposes (the provider row labelled
"FIDO2 WebAuthn" in en_GB / "Passkey" in en); no test currently drives
the webauthn-connector iframe / CDP virtual-authenticator handshake, so
the case throws `Not Implemented` rather than silently no-op'ing.
`setups/user.ts:logUser` and `setups/sso.ts:logUser` now share an
options bag `{ mailBuffer?, twoFactor? }` (the SSO variant's existing
positional `totp` parameter is replaced) and delegate to
`submitTwoFactor` when `twoFactor` is set, keeping the two login
helpers in lock-step.
Refactored consumers:
- `login.spec.ts:Authenticator 2fa` -> the inline 2FA block collapses to
`await logUser(test, page, user, { twoFactor: { kind: 'totp', totp } })`.
- `login.smtp.spec.ts:2fa` -> ditto with `{ kind: 'mail2fa', mailBuffer }`.
- `sso_login.spec.ts:SSO login with TOTP 2fa` -> same `totp` variant.
- `sso_login.smtp.spec.ts:Log and disable` -> same `mail2fa` variant.
- Positional-mailBuffer callers (`login.smtp.spec.ts:Login`,
`organization.smtp.spec.ts:Confirm invited user` and
`Organization is visible`) switch to options-bag
`logUser(..., { mailBuffer })`.
login.spec.ts loses no-longer-needed `expect` / `OTPAuth` imports; the
factor-specific timestamp logic moves into `submitTwoFactor`.
No behaviour change in any test; only structure.
The suite had several locator patterns scattered across helpers and specs;
changes to the bundled web vault would require touching N call sites for
each. This commit funnels them into shared helpers so the next web-vault
update only touches one place per pattern.
`setups/user.ts` additions:
- `openAvatarMenu(page, userName)` — header avatar menu open, anchored on
the user's display name (`{ exact: true }` to avoid cipher-name
substring matches).
- `fillNewMasterPassword(page, password)` — registration / MP-change form's
`newPassword` + `newPasswordConfirm` `formcontrolname` inputs (the three
labels containing "Master password" make label-based locators ambiguous).
- `submitMasterPasswordVerification(page, mp)` — the in-dialog
`app-user-verification` master-password gate (the `<input id="masterPassword">`
inside any sensitive-operation dialog: 2FA enrol/disable, passkey
enrol/remove, key rotation, KDF change). Presses Enter on the input to
avoid the multi-`Continue`-button ambiguity that the current bundled
vault renders.
- `createAccount` switched to `fillNewMasterPassword`.
`setups/2fa.ts` additions + refactor:
- `gotoTwoStepLogin(page, userName)` — Settings → Security → Two-step login
navigation, used by every 2FA enrol/disable function.
- `clickTwoFactorProviderManage(page, providerLabel)` — `bit-item` provider
row → Manage button. Accepts string or RegExp for the row's hasText.
- `activateTOTP` / `disableTOTP` / `activateEmail` / `disableEmail` all
rewritten to use the new helpers, removing the inline duplication.
`setups/sso.ts:logNewUser` — uses `fillNewMasterPassword`.
`organization.smtp.spec.ts` and `sso_organization.smtp.spec.ts` invited-with-
new-account flows — use `fillNewMasterPassword`.
Incidental fixes spotted while refactoring:
- `disableTOTP` / `disableEmail` previously had `getByRole('button',
{ name: 'Test' })` hardcoded for the avatar menu — broke for any user
not named "Test". Now `openAvatarMenu(page, user.name)`, parameterised.
- `activateTOTP` declared its return type as `: OTPAuth.TOTP` (an async
function actually returns `Promise<OTPAuth.TOTP>`); `retrieveEmailCode`
similarly declared `: string` instead of `: Promise<string>`. Both
fixed.
Post-refactor scatter check (rg-confirmed):
- `formcontrolname="newPassword"` outside setups/user.ts: 0
- `input#masterPassword` outside setups/user.ts: 0
- `bit-item` provider-row pattern outside setups/2fa.ts: 0
- Avatar-menu via `name: user.name` outside setups/user.ts: 0
The bundled web vault renames the org-invitation-acceptance toast from
"Invitation accepted" to "Successfully accepted your invitation" in the
post-MP-unlock flow used by both invited-with-new-account and
invited-with-existing-account tests. Update both specs to match the new
toast text.
The SSO-side logNewUser flow still emits "Invitation accepted" for the
SSO account-creation toast, so setups/sso.ts is left untouched.
The SSO invited-with-existing-account flow also no longer auto-redirects
to Keycloak — existing emails land on the email-prefilled login form and
require an explicit "Use single sign-on" click. Add it before the
Keycloak heading assertion.
The product-switch links in the side nav ("Password Manager", "Admin
Console", "Members") are icon-only on the current bundled web vault —
the link element carries the accessible name but no visible text
content. `locator('a').filter({ hasText: '…' })` therefore matches
nothing, and every spec that calls into `setups/orgs.ts` (org create,
member invite, policy edit, …) times out before doing anything.
Switch to `getByRole('link', { name: '…' })` for the three navs.
"Admin Console" appears twice once an org exists (in both
`bit-nav-logo` and `navigation-product-switcher`); `.first()` picks the
visible one. The "Members" entry inside an org also moved from a
`<div>` to a `<link>`, so the `locator('div').filter(...).nth(2)`
selector is replaced with the same role-based selector. The
org-switcher row has a hover tooltip that intercepts the click on the
bundled vault, so the click is forced past the overlay.
Verified empirically: with these changes, `organization.spec.ts` (1/1)
and `sso_organization.spec.ts` (4/5; the remaining failure is an
unrelated server-side master-password-policy enforcement issue) run
green where they previously failed before any helper step.
`playwright/.gitignore` already excludes `test-results/` and
`playwright-report/` relative to `playwright/`. But playwright
artifacts can land at the repo root if the suite is ever invoked
from there (e.g., via a debug script that imports playwright
directly outside the test runner), leaving an untracked
`./test-results/.last-run.json` cluttering `git status`.
Adding the same entries at the repo-root .gitignore catches those
cases too — defensive and zero behavior change.
The bundled web vault refuses to submit registration and login
requests over plain HTTP, surfacing "Insecure URL not allowed. All
URLs must use HTTPS." in the UI. The Continue button is left
`bit-aria-disable=true` and click handlers are no-ops, which
manifests in tests as `locator.fill: timeout exceeded` deep into
createAccount — diagnosed via DOM dump showing the error banner.
Make the test Rocket server actually serve HTTPS:
- Generate a self-signed cert in the Vaultwarden runtime image
(separate RUN layer from the apt install so cert tweaks don't
bust the deps layer cache).
- Point `ROCKET_TLS` at the cert + key in test.env and the
dev .env.template.
- Switch DOMAIN to `https://localhost:${ROCKET_PORT}`.
- Tell Playwright to ignore HTTPS errors on the self-signed cert
(in both `playwright.config.ts` for test contexts and
`global-utils.ts` for the manual context startVault uses to
poll for vault readiness).
Self-signed + `ignoreHTTPSErrors` is the idiomatic Playwright pattern
for a local-only test target; importing a custom CA into each
browser's profile would be substantially more invasive (Firefox uses
NSS, Chromium has its own store) for no real-world fidelity gain.
The `activateTOTP` helper had three issues against the current
bundled web vault (v2026.4.1):
1. The master-password reprompt's Continue button click was racing
form validation: the form uses Angular's `updateOn: 'blur'` so
clicking immediately after `fill()` saw an "invalid" form and the
click was silently no-op. Submitting via `mpInput.press('Enter')`
triggers the form's `ngSubmit` directly, which validates and
submits in one go.
2. `getByLabel('Key').innerText()` was ambiguous: the same page has
a `<bit-svg aria-label="Yubico OTP security key">` providers entry
whose accessible name contains "Key" as a substring. Strict-mode
violation on Firefox. Anchor with `{ exact: true }` to pick only
the `<code aria-label="Key">` element holding the base32 secret.
3. After clicking "Turn on", the original code was effectively a
no-op (`await page.getByRole('heading', { name: 'Turned on' })`
creates a Locator without awaiting visibility), then clicked the
"Close" button which is `bit-aria-disable=true` until the dialog
finishes its transition. Chromium happens to tolerate the early
click; Firefox doesn't. Wait for `networkidle` after Turn on so
the activation request completes; drop the Close click because
the dialog auto-closes on success on this web vault.
`disableTOTP` got the same reprompt-via-Enter fix for parity.
Verified empirically: with these changes, `login.spec.ts`'s
`Authenticator 2fa` test passes on Firefox against bundled
web-vault v2026.4.1.
The bundled web vault dropped the "(required)" suffix from the
master-password input's label text (it likely became a separate visual
indicator). Every test that drives a master-password reprompt — account
creation, TOTP setup, email-2FA setup, SSO master-password flows,
organization-policy management — has been failing on the current
bundled vault because `getByLabel('Master password (required)',
{ exact: true })` no longer matches anything.
Two-line change in most files: switch to `getByLabel('Master password')`,
matching the pattern that `logUser` (setups/user.ts:46) and
`login.spec.ts:40` already use successfully. Substring matching is
case-sensitive in Playwright, so the selector is unambiguous against
the confirm-field ("Confirm master password" has a lowercase 'm').
`createAccount` step 2 (setups/user.ts) and `logNewUser` step 3
(setups/sso.ts) are special: the registration / "Join organisation"
form has three labels matching "Master password" as a case-insensitive
substring ("Master password\n(required)", "Confirm master password\n
(required)" which matches because Playwright's substring match is
case-insensitive in practice, and "Master password hint" which also
matches). Anchor those two fields by their stable `formcontrolname`
attribute (`newPassword` / `newPasswordConfirm`) instead of label text.
Verified empirically: with these changes, both `login.spec.ts` (3/3:
Account creation, Master password login, Authenticator 2fa) and
`sso_login.spec.ts` (8/8 including SSO Account creation, SSO login,
SSO login with TOTP 2fa, Non-SSO login fallback, SSO_ONLY, no SSO)
run green against bundled web-vault v2026.4.1.
* Update to Rust 2024 Edition
Updated to the Rust 2024 Edition and added and fixed several lint checks.
This is a large change which, because of the extra lints, added some possible fixes for issues.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Reorder and merge imports
Signed-off-by: BlackDex <black.dex@gmail.com>
* Remove "db_run!" macro calls where possible
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update crates and gha
Updated all the crates
Updated GitHub Actions
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix restoring revoked user
A new endpoint is used to restore a revoked user.
This commit fixes that.
Fixes#7224
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update datatables
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
* Panic on unrecognised DATABASE_URL instead of silent SQLite fallback
Previously, any DATABASE_URL that did not match the mysql: or postgresql:
prefix was silently treated as a SQLite file path. This caused data loss
in containerised environments when the URL was misconfigured (typos,
quoting issues), as vaultwarden would create an ephemeral SQLite database
that was wiped on restart.
Now, an explicit sqlite:// prefix is supported and used as the default.
Bare paths without a recognised scheme are still accepted for backwards
compatibility, but only if the database file already exists. If not, the
process panics with a clear error message.
Relates to #2835, #1910, #860.
* Use err!() instead of panic!() for unrecognised DATABASE_URL
Follow the established codebase convention where configuration
validation errors use err!() to propagate gracefully, rather than
panic!(). The error propagates through from_config() and is caught
by create_db_pool() which logs and calls exit(1).
* Use 'scheme' instead of 'prefix' in DATABASE_URL messages
Per review feedback, 'scheme' is the more accurate term for the
sqlite:// portion of the URL.
* deps: upgrade the reqwest stack to 0.13
The reqwest 0.13 rustls feature selects the aws-lc provider. Use
rustls-no-provider instead, add rustls 0.23 with the ring provider, and
install that provider at process startup. This keeps Vaultwarden on the
existing ring crypto provider while giving reqwest, OpenDAL and lettre a
process-wide rustls provider.
Disable openidconnect default features and provide a small
AsyncHttpClient wrapper around Vaultwarden's shared reqwest client
builder. This preserves custom DNS, request blocking, timeouts and the
no-redirect OIDC behavior without openidconnect enabling its own reqwest
stack.
Upgrade yubico_ng to 0.15.0 and OpenDAL to 0.56.0. OpenDAL 0.56 also
moves S3 signing to reqsign 3, so switch the optional S3 dependencies
from reqsign/anyhow to reqsign-core and reqsign-aws-v4 and adapt the AWS
SDK credential bridge to the new ProvideCredential API.
Adjust the local OpenDAL call sites for the 0.56 API: use the FS_SCHEME
constant for filesystem checks and replace deprecated remove_all() with
delete_with(...).recursive(true) for Send file cleanup.
* storage: add OpenDAL S3 URI options
OpenDAL S3 storage accepts bucket and root path data today, but
serverless deployments also need URI query parameters to describe provider
behavior in one DATA_FOLDER value.
Update OpenDAL to 0.56.0 and build S3 operators with
S3Config::from_uri(). Keep Vaultwarden's AWS SDK credential chain by
installing a reqsign provider when the URI does not explicitly request
OpenDAL-native credential handling.
Move path handling and operator construction into storage.rs so S3-specific
parsing, credential setup, and URI path manipulation stay out of
configuration handling. Local filesystem behavior is unchanged, and S3
child paths are derived before query strings.
- Update crates including fixing a regression of Diesel
- Update web-vault to v2026.4.1
- Adjusted the README to address the secure context and needing HTTPS
Fixes#7132Closes#7137
Signed-off-by: BlackDex <black.dex@gmail.com>
Keeping the default behaviour of SQLite being built statically,
so as not to break anyone's workflow, but allowing for downstream
packagers to link dynamically against SQLite (where it's fine because
that's the point of package managers).
Note that SQLite is still *not* enabled by default, thanks to the `?` operator.
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
`Cipher::to_json()` returns `Result<Value, Error>` but its match arm for
unknown `atype` values called `panic!("Wrong type")` instead of
propagating an error. This means if a cipher with an invalid/unknown type
ends up in the database (via direct DB edits, data migration issues, or
future type additions in the upstream Bitwarden protocol), the entire
server process would crash on the next sync request.
Replace the `panic!` with `err!()` so callers receive a proper `Err` and
can handle or log it gracefully without taking down the server.
Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
The collection update endpoints (post_collections_update and
post_collections_admin) call .unwrap() on cipher.organization_uuid
in four places. If a user-owned cipher without an organization
somehow reaches these code paths, the server would panic.
Extract the organization UUID early with a descriptive error message
instead of relying on .unwrap(), preventing potential panics and
providing a clear API error response.
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Add archiving
* Update Diesel macros and remove unnecessary SUPPORTED_FEATURE_FLAG
* Add IF EXISTS to down.sql migratinos
* Rename migration folders, separate logic based on PR threads
* Ensure SSO token is only usable on the same client
This commit adds an extra check via cookies to ensure the same browser/client is used to request and provide the SSO token.
Previously it would be able to provide a custom link which attackers could use to steal data.
While an attacker would still need the Master Password to be able to decrypt or execute specific actions, they were able to fetch encrypted data.
Solved with some help of Claude Code.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Check email-verified on SSO login/create
This commit prevents possible account takeover via SSO which doesn't check/validate or provide validated status of the email.
It was checked at other locations, but was skipped here.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Prevent data disclosure via SSO endpoints
This commit prevents some data disclosure and user enumeration by only returning the fake SSO identifier.
Since we do not check the identifier anywhere useful, returning the fake one is just fine.
During an invite to an org, that link contains the correct UUID and will be used for the master password requirements.
For anything else, server admins should set the `SSO_MASTER_PASSWORD_POLICY` env variable.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Adjust admin layout to fix issues when SSO is enabled
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
IPv4 addresses can also be in decimal or hex formats.
These were not checked during the Global IP check, and could bypass it.
We now convert everything to the right format before running this check and it will catch these formats.
Also updated the `is_global()` function to match Rust's still unstable version.
And updated the Image Magic checks to be more precise and filter out any possible broken or invalid formats.
While at it, also added several checks to ensure these special formatted IPv4 addresses are still blocked and punycode domains are also correctly resolved.
Signed-off-by: BlackDex <black.dex@gmail.com>
Quote from the lint description:
"More flexibility, better memory optimization, and more idiomatic Rust code.
&Option<T> in a function signature breaks encapsulation because the caller must own T and move it into an Option to call with it. When returned, the owner must internally store it as Option<T> in order to return it. At a lower level, &Option<T> points to memory with the presence bit flag plus the T value, whereas Option<&T> is usually optimized to a single pointer, so it may be more optimal."
Quote from lint description:
"Using a smaller unit for a duration that is evenly divisible by a larger unit reduces readability. Readers have to mentally convert values, which can be error-prone and makes the code less clear."
- Updated web-vault to v2026.3.1
Added a new endpoint needed for the admin console to work
- Updated all crates including webpki CVE fixes - Closes#7115
- Updated GHA
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update Rust, Crates and GHA
- Updated Rust to v1.95.0
- Updated all the crates
- Update GitHub Actions
With the crate updates, hickory-resolver was updated which needed some changes.
During testing I found a bug with the fallback resolving from Tokio.
The resolver doesn't work if it receives only a `&str`, it needs a `port` too.
This fixed the resolving if Hickory failed to load.
Also, Hickory switched the resolving to prefer IPv6. While this is nice, it could break or slowdown resolving for IPv4 only environments.
Since we already have a flag to prefer IPv6, we check if this is set, else resolve IPv4 first and IPv6 afterwards.
Also, we returned just 1 IpAddr record, and ignored the rest. This could mean, a failed attempt to connect if the first IP endpoint has issues.
Same if the first records is IPv6 but the server doesn't support this, it never tried a possible returned IPv4 address.
We now return a full list of the resolved records unless one of the records matched a filtered address, than the whole resolving is ignored as was previously the case.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Adjust resolver builder path
Changed the way the resolver is constructed.
This way the default is always selected no matter which part of the hickory build fails.
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>