[PR #7297] [CLOSED] Add webauthn login (consolidated) #27485

Closed
opened 2026-06-15 14:47:44 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/dani-garcia/vaultwarden/pull/7297
Author: @zaid-marji
Created: 6/3/2026
Status: Closed

Base: mainHead: webauthn-login


📝 Commits (10+)

  • c0589bb playwright: fix stale Master password selectors against bundled web vault
  • c02ced4 playwright: fix TOTP setup flow against bundled web vault
  • f30847d playwright: serve the test Vaultwarden over HTTPS
  • a3fb027 gitignore: catch stray playwright artifacts at the repo root
  • 9186ec2 playwright: fix stale org-nav selectors against bundled web vault
  • 0c009d6 playwright: fix invitation-accepted toast text + SSO existing-account redirect
  • ad582b4 playwright: centralize web-vault selectors into shared setup helpers
  • 88ab514 playwright: centralize 2FA challenge handling
  • 1cdf0b7 Feat add webauthn login
  • 5760ade Fix and harden the passkey login implementation

📊 Changes

57 files changed (+6365 additions, -202 deletions)

View changed files

📝 .env.template (+4 -0)
📝 .gitignore (+6 -0)
📝 Cargo.toml (+2 -1)
📝 README.md (+4 -0)
migrations/mysql/2026-02-12-000000_add_web_authn_credentials/down.sql (+1 -0)
migrations/mysql/2026-02-12-000000_add_web_authn_credentials/up.sql (+15 -0)
migrations/mysql/2026-05-21-000000_add_web_authn_login_challenges/down.sql (+1 -0)
migrations/mysql/2026-05-21-000000_add_web_authn_login_challenges/up.sql (+7 -0)
migrations/postgresql/2026-02-12-000000_add_web_authn_credentials/down.sql (+1 -0)
migrations/postgresql/2026-02-12-000000_add_web_authn_credentials/up.sql (+14 -0)
migrations/postgresql/2026-05-21-000000_add_web_authn_login_challenges/down.sql (+1 -0)
migrations/postgresql/2026-05-21-000000_add_web_authn_login_challenges/up.sql (+7 -0)
migrations/sqlite/2026-02-12-000000_add_web_authn_credentials/down.sql (+1 -0)
migrations/sqlite/2026-02-12-000000_add_web_authn_credentials/up.sql (+14 -0)
migrations/sqlite/2026-05-21-000000_add_web_authn_login_challenges/down.sql (+1 -0)
migrations/sqlite/2026-05-21-000000_add_web_authn_login_challenges/up.sql (+7 -0)
📝 playwright/.env.template (+2 -1)
📝 playwright/README.md (+7 -0)
📝 playwright/compose/playwright/Dockerfile (+1 -1)
📝 playwright/compose/warden/Dockerfile (+12 -0)

...and 37 more files

📄 Description

Summary

Adds the long-requested "Log in with passkey" feature: passwordless
account login via a discoverable WebAuthn credential, with optional
PRF-based vault unlock. This consolidates the prior efforts in #6820
(@samos667) and #5929 (@zUnixorn) into a single review-ready PR, as the
maintainers suggested on those threads. The foundational commit retains
@samos667 as author and credits @zUnixorn via Co-authored-by:; the
remaining commits are my own.

What it does

  • Passwordless login (grant_type=webauthn): a discoverable, user-verified
    WebAuthn assertion authenticates the user. Per Bitwarden's model the
    authenticator's user-verification is the second factor, so TOTP is not
    re-prompted.
  • PRF vault unlock: a passkey can carry a wrapped key set so the vault can
    be unlocked with the passkey. Exposed via webAuthnPrfOptions on /sync
    and UserDecryptionOptions at login, matching the Bitwarden web client.
  • Passkey management: enroll, list, and delete account passkeys
    (/api/webauthn*), each gated by password/OTP.
  • Key rotation: post_rotatekey re-wraps the account-key-wrapped PRF
    material so rotating the account key no longer silently breaks passkey unlock.
  • prfStatus implemented (Enabled / Supported / Unsupported), replacing
    the previous placeholder.
  • Availability advertised via the pm-2035-passkey-unlock config flag and
    suppressed under SSO_ONLY or a WebAuthn-incompatible DOMAIN.

Threat model

The login and key-handling paths defend against:

  • Replay of a captured assertion (single-use transactional login
    challenges; signature-counter advance with CAS rollback protection).
  • Account/credential enumeration via the unauthenticated grant
    (uniform Passkey authentication failed response across every failure
    branch — unknown user, missing credential, verification failure, account
    disabled or unverified — gated by an IP-shared login rate limit).
  • Cross-user credential misuse (every passkey CRUD and read is
    per-user-scoped at the DB layer; credential ID hashes are unique per user
    rather than global).
  • Passkey-unlock breakage on key rotation (post_rotatekey re-wraps each
    PRF credential's account-key-wrapped material under the new account key as
    part of the rotation; a concurrent passkey delete during rotation is
    tolerated as a best-effort degraded no-op).
  • Audit-log poisoning by anonymous probes (pre-verification failures
    deliberately omit the UserFailedLogIn event so an attacker submitting
    forged user-handles cannot create audit entries on the victim's account).

Hardening

A few changes also tighten shared paths beyond passkey login — flagged here
since they affect more than the new feature:

  • 2FA-policy enforcement is now fail-closed (enforce_2fa_policy): a
    transient DB error while loading a user's Require-2FA org memberships
    previously collapsed to an empty set and silently skipped enforcement; it now
    propagates. This closes the same gap for password login and recovery-code use.
  • SSO_ONLY is enforced deny-by-default at the token endpoint across all
    grant types, replacing the previous password-only per-grant check.
  • The 2FA-WebAuthn endpoints and is_webauthn_2fa_supported are hardened
    against a malformed DOMAIN
    that could previously panic a worker thread
    (.expect() → graceful refusal); every WebAuthn entry point is now gated.
  • Several shared DB lookups now return Result instead of swallowing
    errors into None, so a transient failure surfaces rather than being misread
    as "not found".

Database

Two new additive tables (web_authn_credentials,
web_authn_login_challenges) with migrations for all three backends. No
existing data is migrated — webauthn-rs 0.5 is already in main, so the
2FA-object migration discussed on #5929 does not apply here.

Testing

  • Builds on #7248, which brought the Playwright suite back to green against the
    current bundled web-vault; the new E2E specs run on that harness, so whoever
    reviews/merges this may want to take #7248 alongside it. The first 8 commits
    of this PR's diff are #7248's; the webauthn-specific work starts at
    1cdf0b7b.
  • cargo fmt / clippy / test and the Playwright E2E suite pass. New Rust
    unit tests cover the PRF/UserDecryption response shapes, challenge
    freshness + single-use, and passkey-rotation validation.
  • Playwright coverage: passkey login, PRF enrolment + unlock, the full
    enrol → login → lock → unlock → rotate → remove lifecycle, the rejection
    paths (disabled account, deleted credential, runtime-unusable 2FA), SSO_ONLY
    denial, and malformed-input rejection.

Compatibility

Wire shapes (UserDecryptionOptions, webAuthnPrfOptions, GetPrfStatus)
follow the upstream Bitwarden server so the official web client works
unchanged.


I'm glad to answer questions or make any changes needed to help move this
along — just let me know.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/dani-garcia/vaultwarden/pull/7297 **Author:** [@zaid-marji](https://github.com/zaid-marji) **Created:** 6/3/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `webauthn-login` --- ### 📝 Commits (10+) - [`c0589bb`](https://github.com/dani-garcia/vaultwarden/commit/c0589bbd749b3e7dcdf47e50c253c94968426ee6) playwright: fix stale Master password selectors against bundled web vault - [`c02ced4`](https://github.com/dani-garcia/vaultwarden/commit/c02ced432dc5feabc0ba7078390a5664a08910e0) playwright: fix TOTP setup flow against bundled web vault - [`f30847d`](https://github.com/dani-garcia/vaultwarden/commit/f30847d15e4b86671829ffd3ece180e18414772f) playwright: serve the test Vaultwarden over HTTPS - [`a3fb027`](https://github.com/dani-garcia/vaultwarden/commit/a3fb02776dd1b12108869df7dd6141b1c07c930a) gitignore: catch stray playwright artifacts at the repo root - [`9186ec2`](https://github.com/dani-garcia/vaultwarden/commit/9186ec245b0127e314c2ffcc1ff790fda79804f2) playwright: fix stale org-nav selectors against bundled web vault - [`0c009d6`](https://github.com/dani-garcia/vaultwarden/commit/0c009d679d8a61ecfa47f142511037d02711a803) playwright: fix invitation-accepted toast text + SSO existing-account redirect - [`ad582b4`](https://github.com/dani-garcia/vaultwarden/commit/ad582b460acaaa3199ad3b3f6189c7a90c139cd9) playwright: centralize web-vault selectors into shared setup helpers - [`88ab514`](https://github.com/dani-garcia/vaultwarden/commit/88ab51443a4f84b6d239263f78663c9648d6b97e) playwright: centralize 2FA challenge handling - [`1cdf0b7`](https://github.com/dani-garcia/vaultwarden/commit/1cdf0b7b86066cef9758e84c591aa3791b76e39d) Feat add webauthn login - [`5760ade`](https://github.com/dani-garcia/vaultwarden/commit/5760ade3258ae92fc855430f5cd45211c8486d33) Fix and harden the passkey login implementation ### 📊 Changes **57 files changed** (+6365 additions, -202 deletions) <details> <summary>View changed files</summary> 📝 `.env.template` (+4 -0) 📝 `.gitignore` (+6 -0) 📝 `Cargo.toml` (+2 -1) 📝 `README.md` (+4 -0) ➕ `migrations/mysql/2026-02-12-000000_add_web_authn_credentials/down.sql` (+1 -0) ➕ `migrations/mysql/2026-02-12-000000_add_web_authn_credentials/up.sql` (+15 -0) ➕ `migrations/mysql/2026-05-21-000000_add_web_authn_login_challenges/down.sql` (+1 -0) ➕ `migrations/mysql/2026-05-21-000000_add_web_authn_login_challenges/up.sql` (+7 -0) ➕ `migrations/postgresql/2026-02-12-000000_add_web_authn_credentials/down.sql` (+1 -0) ➕ `migrations/postgresql/2026-02-12-000000_add_web_authn_credentials/up.sql` (+14 -0) ➕ `migrations/postgresql/2026-05-21-000000_add_web_authn_login_challenges/down.sql` (+1 -0) ➕ `migrations/postgresql/2026-05-21-000000_add_web_authn_login_challenges/up.sql` (+7 -0) ➕ `migrations/sqlite/2026-02-12-000000_add_web_authn_credentials/down.sql` (+1 -0) ➕ `migrations/sqlite/2026-02-12-000000_add_web_authn_credentials/up.sql` (+14 -0) ➕ `migrations/sqlite/2026-05-21-000000_add_web_authn_login_challenges/down.sql` (+1 -0) ➕ `migrations/sqlite/2026-05-21-000000_add_web_authn_login_challenges/up.sql` (+7 -0) 📝 `playwright/.env.template` (+2 -1) 📝 `playwright/README.md` (+7 -0) 📝 `playwright/compose/playwright/Dockerfile` (+1 -1) 📝 `playwright/compose/warden/Dockerfile` (+12 -0) _...and 37 more files_ </details> ### 📄 Description ## Summary Adds the long-requested **"Log in with passkey"** feature: passwordless account login via a discoverable WebAuthn credential, with optional PRF-based vault unlock. This consolidates the prior efforts in #6820 (@samos667) and #5929 (@zUnixorn) into a single review-ready PR, as the maintainers suggested on those threads. The foundational commit retains @samos667 as author and credits @zUnixorn via `Co-authored-by:`; the remaining commits are my own. ## What it does - **Passwordless login** (`grant_type=webauthn`): a discoverable, user-verified WebAuthn assertion authenticates the user. Per Bitwarden's model the authenticator's user-verification is the second factor, so TOTP is not re-prompted. - **PRF vault unlock**: a passkey can carry a wrapped key set so the vault can be unlocked with the passkey. Exposed via `webAuthnPrfOptions` on `/sync` and `UserDecryptionOptions` at login, matching the Bitwarden web client. - **Passkey management**: enroll, list, and delete account passkeys (`/api/webauthn*`), each gated by password/OTP. - **Key rotation**: `post_rotatekey` re-wraps the account-key-wrapped PRF material so rotating the account key no longer silently breaks passkey unlock. - **`prfStatus`** implemented (Enabled / Supported / Unsupported), replacing the previous placeholder. - Availability advertised via the `pm-2035-passkey-unlock` config flag and suppressed under `SSO_ONLY` or a WebAuthn-incompatible `DOMAIN`. ## Threat model The login and key-handling paths defend against: - **Replay** of a captured assertion (single-use transactional login challenges; signature-counter advance with CAS rollback protection). - **Account/credential enumeration** via the unauthenticated grant (uniform `Passkey authentication failed` response across every failure branch — unknown user, missing credential, verification failure, account disabled or unverified — gated by an IP-shared login rate limit). - **Cross-user credential misuse** (every passkey CRUD and read is per-user-scoped at the DB layer; credential ID hashes are unique per user rather than global). - **Passkey-unlock breakage on key rotation** (`post_rotatekey` re-wraps each PRF credential's account-key-wrapped material under the new account key as part of the rotation; a concurrent passkey delete during rotation is tolerated as a best-effort degraded no-op). - **Audit-log poisoning by anonymous probes** (pre-verification failures deliberately omit the `UserFailedLogIn` event so an attacker submitting forged user-handles cannot create audit entries on the victim's account). ## Hardening A few changes also tighten **shared** paths beyond passkey login — flagged here since they affect more than the new feature: - **2FA-policy enforcement is now fail-closed** (`enforce_2fa_policy`): a transient DB error while loading a user's Require-2FA org memberships previously collapsed to an empty set and silently skipped enforcement; it now propagates. This closes the same gap for password login and recovery-code use. - **SSO_ONLY is enforced deny-by-default** at the token endpoint across all grant types, replacing the previous password-only per-grant check. - **The 2FA-WebAuthn endpoints and `is_webauthn_2fa_supported` are hardened against a malformed `DOMAIN`** that could previously panic a worker thread (`.expect()` → graceful refusal); every WebAuthn entry point is now gated. - **Several shared DB lookups now return `Result`** instead of swallowing errors into `None`, so a transient failure surfaces rather than being misread as "not found". ## Database Two new **additive** tables (`web_authn_credentials`, `web_authn_login_challenges`) with migrations for all three backends. No existing data is migrated — `webauthn-rs` 0.5 is already in `main`, so the 2FA-object migration discussed on #5929 does not apply here. ## Testing - Builds on #7248, which brought the Playwright suite back to green against the current bundled web-vault; the new E2E specs run on that harness, so whoever reviews/merges this may want to take #7248 alongside it. The first 8 commits of this PR's diff are #7248's; the webauthn-specific work starts at `1cdf0b7b`. - `cargo fmt` / `clippy` / `test` and the Playwright E2E suite pass. New Rust unit tests cover the PRF/UserDecryption response shapes, challenge freshness + single-use, and passkey-rotation validation. - Playwright coverage: passkey login, PRF enrolment + unlock, the full enrol → login → lock → unlock → rotate → remove lifecycle, the rejection paths (disabled account, deleted credential, runtime-unusable 2FA), SSO_ONLY denial, and malformed-input rejection. ## Compatibility Wire shapes (`UserDecryptionOptions`, `webAuthnPrfOptions`, `GetPrfStatus`) follow the upstream Bitwarden server so the official web client works unchanged. --- I'm glad to answer questions or make any changes needed to help move this along — just let me know. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-06-15 14:47:44 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/vaultwarden#27485