Tightens the passkey-management endpoints against concurrent-mutation
races, stale challenges, and token-replay; routes the unauthenticated-
login path through atomic challenge consumption; and pins the behaviour
with new test coverage.
Atomic challenge consumption:
- `WebAuthnLoginChallenge::take` performs a transactional
SELECT+DELETE on the challenge row, replacing the previous "find
then delete" pattern that could leave a stale challenge consumable
across tabs.
- `TwoFactor::take_by_user_and_type` does the same for the
passkey-management registration / assertion challenges, replacing
the prior `find_by_user_and_type` + `tf.delete` pattern at every
passkey-management call site.
Challenge state binding:
- `passkey_management_challenge_is_fresh` enforces a TTL + clock-skew
window on persisted challenge rows.
- `passkey_registration_challenge_state` /
`passkey_assertion_challenge_state` enforce per-request token
equality, freshness, and user `security_stamp` equality before
unwrapping the saved webauthn-rs state. A password change
mid-ceremony invalidates the in-flight challenge; a stale tab
cannot consume a fresh one.
Identity.rs (`webauthn_login`):
- Consume the challenge via `WebAuthnLoginChallenge::take` before any
cryptographic verification, so a malformed `device_response` cannot
poison subsequent grant attempts that share the same token.
- AUTH_FAILED-uniformity: every pre-bind error path returns the same
generic "Passkey authentication failed" rather than leaking the
underlying cause (DB transient vs missing user vs malformed
assertion vs verification failure).
- Drop the local `AssertionResponseCopy` in favour of the shared
`PublicKeyCredentialCopy` from `core::two_factor::webauthn`, so the
passkey-login and 2FA-webauthn paths agree on the serde wire shape
for the assertion response.
Other:
- `ciphers.rs` / `accounts.rs`: `/sync` gates `webAuthnPrfOptions` on
`account_passkeys_allowed`; PRF rotation path tolerates a credential
that was removed mid-rotation.
- `vaultwarden.scss.hbs`: hide the lock-screen "Unlock with passkey"
button until PRF enrolment completes so the affordance only appears
when the server will actually accept it.
- `playwright/tests/passkey.spec.ts`: additional coverage for the
hardened flows.
Server hardening:
- accounts.rs: reject duplicate ids in `passkey_unlock_data` before they
collapse through the HashSet superset check and silently double-apply
with the second write winning.
- accounts.rs: re-check the PRF credential set immediately before
`set_password` commits the new account key. A concurrent enrol between
the validation snapshot and the rewrap loop would otherwise leave the
new credential's `encrypted_user_key` wrapped under the OLD akey,
permanently broken once the rotation commits.
- mod.rs: drop the legacy bare-`PasskeyRegistration` fallback in
`passkey_registration_challenge_state` — no writer produces that
shape, and the fallback permitted token-less finishes against a
hypothetical un-tokened row.
- mod.rs: switch token compares in both challenge-state extractors to
`crypto::ct_eq` (matches the `cred_id` pattern already used at
mod.rs:1424).
- mod.rs: normalise `passkey_assertion_challenge_state`'s deserialise
error to the generic "Invalid assertion challenge" rather than leaking
the underlying serde shape.
- mod.rs: document why `post_api_webauthn_delete` is intentionally
exempt from the SSO_ONLY gate (delete narrows capability, never
grants it; session is still SSO-authed).
- mod.rs: document that the post-`start_passkey_registration` mutation
of `authenticator_selection.require_resident_key` is a client-side
hint only (webauthn-rs destructures the field as unused), so readers
don't mistake it for a server-enforced policy.
Observability:
- web_authn_credential.rs / two_factor.rs: log Diesel errors from the
SELECT+DELETE transactions in `WebAuthnLoginChallenge::take` and
`TwoFactor::take_by_user_and_type` before collapsing to `None`. The
prior `.unwrap_or(None)` made degraded DB behaviour (deadlock, lock
timeout, conn drop) indistinguishable from a normal stale-token
rejection.
- identity.rs: log the failure path of `passkey_transports`'s
`serde_json::to_value(passkey)` instead of silently emitting
`Transports: []`. Clients can't otherwise distinguish "authenticator
reported no transports" from "we failed to serialise the passkey".
UX:
- vaultwarden.scss.hbs: re-add the `.vw-passkey-login` hide under
`sso_enabled && sso_only`. Without it the login page renders the
"Log in with passkey" button under SSO_ONLY, but the server gate at
`identity.rs:1250` rejects the click — UX dead end.
Test reliability:
- passkey.spec.ts: the "Non-PRF passkey" test now logs out + logs back
in via passkey between enrol and the lock-screen check. Without
this refresh the lock-screen reads the pre-enrol cached `/api/sync`
and the assertion passes for the wrong reason (cached
webAuthnPrfOptions was empty before enrol regardless of what the
server stored).
- passkey.spec.ts: guard the three describe blocks that restart the
vault container (`Passkey grant is rejected when SSO_ONLY is on`,
`Passkey enrolment is rejected when SSO_ONLY is on`, `Passkey login
rejects forged unverified-email handles`) on `useExternalVault` so
`PW_USE_EXTERNAL_VAULT=1` (host-cargo iteration) cleanly skips them
instead of colliding with the host process on port 8003.
- passkey.spec.ts: the SSO_ONLY-enrolment `afterAll` now also blanks
`sso_authority`, `sso_client_id`, `sso_client_secret` — the previous
reset left those placeholders in `config.json` and contaminated any
later test that toggled `sso_enabled=true`.
- passkey.spec.ts: the bearer-sniffer in the SSO_ONLY-enrolment
beforeAll now filters on `url.includes('/api/')` instead of capturing
whatever token happened to fly last during createAccount; mirrors
the disciplined sniffer in `account_lifecycle.spec.ts:178` and
insulates the test from web-vault request-order churn.
- 2fa.ts: export `resetTotpTimeStep()`; call it from the
`test.beforeEach` in `account_lifecycle.spec.ts` and `passkey.spec.ts`
so the module-scoped `lastSubmittedTotpTimeStep` doesn't leak across
tests / projects under `workers: 1` (would otherwise force a 30s
wait before the first TOTP of project N+1 based on project N's
history).
- global-utils.ts: document the `resetDB=false` contract honestly —
docker recreates the Vaultwarden container on any env-var change in
the compose `environment:` block, dropping the tmpfs sqlite DB along
with the in-process RSA key.
- login.smtp.spec.ts: drop the redundant "Dismiss extension prompts"
step from the 2fa test. `logUser` already calls
`utils.ignoreExtension` and lands on /vault, so the explicit "Add
it later" click timed out on a button the prior step had already
dismissed.
Test coverage additions:
- accounts.rs rotation validator: add a regression test for the
duplicate-id rejection in `passkey_unlock_data`. With a HashSet-only
superset check the rewrap loop in `post_rotatekey` could silently
apply two updates to the same credential id (second blob wins); the
test pins the new len-equality guard.
- mod.rs registration-challenge state: replace
`legacy_registration_challenge_rejects_finish_token` (which asserted
a "legacy bare-`PasskeyRegistration` accepted without token" fallback
no longer present) with
`registration_challenge_rejects_unwrapped_legacy_state`, matching
the assertion-side test's stricter contract: any blob that's not the
`{token, state}` wrapper is rejected regardless of whether a token
is sent. Pins the token-binding bypass closure.
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.
* 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>
* Use Diesels MultiConnections Derive
With this PR we remove almost all custom macro's to create the multiple database type code. This is now handled by Diesel it self.
This removed the need of the following functions/macro's:
- `db_object!`
- `::to_db`
- `.from_db()`
It is also possible to just use one schema instead of multiple per type.
Also done:
- Refactored the SQLite backup function
- Some formatting of queries so every call is one a separate line, this looks a bit better
- Declare `conn` as mut inside each `db_run!` instead of having to declare it as `mut` in functions or calls
- Added an `ACTIVE_DB_TYPE` static which holds the currently active database type
- Removed `diesel_logger` crate and use Diesel's `set_default_instrumentation()`
If you want debug queries you can now simply change the log level of `vaultwarden::db::query_logger`
- Use PostgreSQL v17 in the Alpine images to match the Debian Trixie version
- Optimized the Workflows since `diesel_logger` isn't needed anymore
And on the extra plus-side, this lowers the compile-time and binary size too.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Adjust query_logger and some other small items
Signed-off-by: BlackDex <black.dex@gmail.com>
* Remove macro, replaced with an function
Signed-off-by: BlackDex <black.dex@gmail.com>
* Implement custom connection manager
Signed-off-by: BlackDex <black.dex@gmail.com>
* Updated some crates to keep up2date
Signed-off-by: BlackDex <black.dex@gmail.com>
* Small adjustment
Signed-off-by: BlackDex <black.dex@gmail.com>
* crate updates
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update crates
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
The newer web-vaults handle the 2fa recovery code differently.
This commit fixes this by adding this new flow.
Fixes#6200Fixes#6203
Signed-off-by: BlackDex <black.dex@gmail.com>
* update webauthn to 0.5
* add basic migration impl
* fix clippy warnings
* clear up `COSEKeyType::EC_OKP` case
* fix TODOs
* use same timeout as in webauthn 0.3 impl
* fix: clippy warnings and formatting
* Update Cargo.toml
Co-authored-by: Daniel <daniel.barabasa@gmail.com>
* Update src/api/core/two_factor/webauthn.rs
Co-authored-by: Daniel <daniel.barabasa@gmail.com>
* Update src/api/core/two_factor/webauthn.rs
Co-authored-by: Daniel <daniel.barabasa@gmail.com>
* Update src/api/core/two_factor/webauthn.rs
Co-authored-by: Daniel <daniel.barabasa@gmail.com>
* regenerate Cargo.lock
* Use securitykey methods
* use CredentialsV3 from webauthn-rs instead of own webauthn_0_3 module
* fix cargo fmt issue
---------
Co-authored-by: Helmut K. C. Tessarek <tessarek@evermeet.cx>
Co-authored-by: Daniel <daniel.barabasa@gmail.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* rename membership
rename UserOrganization to Membership to clarify the relation
and prevent confusion whether something refers to a member(ship) or user
* use newtype pattern
* implement custom derive macro IdFromParam
* add UuidFromParam macro for UUIDs
* add macros to Docker build
Co-authored-by: dfunkt <dfunkt@users.noreply.github.com>
---------
Co-authored-by: dfunkt <dfunkt@users.noreply.github.com>
* Change API inputs/outputs and structs to camelCase
* Fix fields and password history
* Use convert_json_key_lcase_first
* Make sends lowercase
* Update admin and templates
* Update org revoke
* Fix sends expecting size to be a string on mobile
* Convert two-factor providers to string
Since the feature `Login with device` some actions done via the
web-vault need to be verified via an OTP instead of providing the MasterPassword.
This only happens if a user used the `Login with device` on a device
which uses either Biometrics login or PIN. These actions prevent the
athorizing device to send the MasterPasswordHash. When this happens, the
web-vault requests an OTP to be filled-in and this OTP is send to the
users email address which is the same as the email address to login.
The only way to bypass this is by logging in with the your password, in
those cases a password is requested instead of an OTP.
In case SMTP is not enabled, it will show an error message telling to
user to login using there password.
Fixes#4042
Improved sync speed by resolving the N+1 query issues.
Solves #1402 and Solves #1453
With this change there is just one query done to retreive all the
important data, and matching is done in-code/memory.
With a very large database the sync time went down about 3 times.
Also updated misc crates and Github Actions versions.
For a while now WebAuthn has replaced u2f.
And since web-vault v2.27.0 the connector files for u2f have been removed.
Also, on the official bitwarden server the endpoint to `/two-factor/get-u2f` results in a 404.
- Removed all u2f code except the migration code from u2f to WebAuthn
This is a rather large PR which updates the async branch to have all the
database methods as an async fn.
Some iter/map logic needed to be changed to a stream::iter().then(), but
besides that most changes were just adding async/await where needed.
This is a rather large PR which updates the async branch to have all the
database methods as an async fn.
Some iter/map logic needed to be changed to a stream::iter().then(), but
besides that most changes were just adding async/await where needed.
- Decreased `recursion_limit` from 512 to 87
Mainly done by optimizing the config macro's.
This fixes an issue with the rust-analyzer which doesn't go beyond 128
- Removed Regex for masking sensitive values and replaced it with a map()
This is much faster then using a Regex.
- Refactored the get_support_json macro's
- All items above also lowered the binary size and possibly compile-time
- Removed `_conn: DbConn` from several functions, these caused unnecessary database connections for functions who didn't used that at all
- Decreased json response for `/plans`
- Updated libraries and where needed some code changes
This also fixes some rare issues with SMTP https://github.com/lettre/lettre/issues/678
- Using Rust 2021 instead of 2018
- Updated rust nightly
An incomplete 2FA login is one where the correct master password was provided,
but the 2FA token or action required to complete the login was not provided
within the configured time limit. This potentially indicates that the user's
master password has been compromised, but the login was blocked by 2FA.
Be aware that the 2FA step can usually still be completed after the email
notification has already been sent out, which could be confusing. Therefore,
the incomplete 2FA time limit should be long enough that this situation would
be unlikely. This feature can also be disabled entirely if desired.
When using MariaDB v10.5+ Foreign-Key errors were popping up because of
some changes in that version. To mitigate this on MariaDB and other
MySQL forks those errors are now catched, and instead of a replace_into
an update will happen. I have tested this as thorough as possible with
MariaDB 10.5, 10.4, 10.3 and the default MySQL on Ubuntu Focal. And
tested it again using sqlite, all seems to be ok on all tables.
resolves#1081. resolves#1065, resolves#1050
Diesel requires the following changes:
- Separate connection and pool types per connection, the generate_connections! macro generates an enum with a variant per db type
- Separate migrations and schemas, these were always imported as one type depending on db feature, now they are all imported under different module names
- Separate model objects per connection, the db_object! macro generates one object for each connection with the diesel macros, a generic object, and methods to convert between the connection-specific and the generic ones
- Separate connection queries, the db_run! macro allows writing only one that gets compiled for all databases or multiple ones
Because of differences in how .on_conflict() works compared to .replace_into() the PostgreSQL backend wasn't correctly ensuring the unique constraint on user_uuid and atype wasn't getting violated.
This change simply issues a DELETE on the unique constraint prior to the insert to ensure uniqueness. PostgreSQL does not support multiple constraints in ON CONFLICT clauses.
- Added security check for previouse used codes
- Allow TOTP codes with 1 step back and forward when there is a time
drift. This means in total 3 codes could be valid. But only newer codes
then the previouse used codes are excepted after that.
This includes migrations as well as Dockerfile's for amd64.
The biggest change is that replace_into isn't supported by Diesel for the
PostgreSQL backend, instead requiring the use of on_conflict. This
unfortunately requires a branch for save() on all of the models currently
using replace_into.