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>
If the refresh token is invalid or expired we need to return a specific JSON and HTTP Status, else the clients will not logout.
Fixes#7060Closes#7080
Signed-off-by: BlackDex <black.dex@gmail.com>