Skip to content

add optional WebAuthn 2FA support#334

Open
aokellermann wants to merge 5 commits intodoy:mainfrom
aokellermann:aokellermann/webauthn
Open

add optional WebAuthn 2FA support#334
aokellermann wants to merge 5 commits intodoy:mainfrom
aokellermann:aokellermann/webauthn

Conversation

@aokellermann
Copy link
Copy Markdown

Manually tested with a Yubikey 5C NFC.

See #116 for previous work on this.

Rebases doy#116 onto current main and upgrades the webauthn-rs stack
from the PR's 0.5.0-dev git pins to crates.io 0.5.4 stable.

Migrates Error::TwoFactorRequired and ConnectErrorRes to a HashMap keyed
by TwoFactorProviderType so the per-provider WebAuthn challenge
(PublicKeyCredentialRequestOptions) from TwoFactorProviders2 can be
threaded through to the agent's 2FA flow. Origin for the assertion comes
from Config::ui_url() so it works for default-cloud accounts where
base_url is unset.

Gated behind the off-by-default `webauthn` cargo feature; non-feature
builds use a stub PublicKeyCredentialRequestOptions to keep the type
signatures identical.
Bitwarden's API expects camelCase `clientDataJson` (vs. webauthn-rs-proto's
W3C-spec `clientDataJSON`) and a non-nullable `appid: bool` in the
extensions object. The previous approach serialized the upstream type and
patched the string; this only worked when both extension fields happened
to be `None` in a specific order, and would silently break on any
upstream change.

Introduce a dedicated `BitwardenAssertion` wire type with the exact shape
the server wants and convert `PublicKeyCredential` into it before
serializing.

Also clean up the WebAuthn pinentry prompt strings (dedicated header /
description with a touch reminder), drop a stray `eprintln!` from
`request_touch` in favor of `log::debug!`, and pull in `base64urlsafedata`
behind the `webauthn` feature for the new wire type's byte fields.
Three fixes on top of the initial WebAuthn support:

* Restore the legacy `TwoFactorProviders` Vec deserialization and read the
  per-provider challenge data from a separate `TwoFactorProviders2` field
  (now `providers_data` on `Error::TwoFactorRequired`). The previous
  combined HashMap shape regressed login for any server response that only
  emitted the legacy array (older Vaultwarden, malformed challenges), and
  affected non-WebAuthn builds too. Provider dispatch is once again driven
  by the Vec; the HashMap is consulted only to pull the WebAuthn challenge
  for the WebAuthn arm.

* Derive the assertion origin from the challenge's `rp_id` rather than
  `Config::ui_url()`. The authenticator only requires the origin host to
  match (or be a subdomain of) `rp_id`, and Bitwarden registers credentials
  against the vault host it serves; using `rp_id` directly avoids silent
  signature rejection on self-hosted setups where the user's configured
  `ui_url` doesn't line up with the registered host.

* Wrap the synchronous `perform_auth` USB HID call in
  `tokio::task::block_in_place` so a 60s wait on the security key doesn't
  starve other tasks on the agent's tokio worker. `spawn_blocking` is not
  usable here because `CtapAuthenticator` borrows from the local
  `UiCallback` and is not `'static`.
The 60s perform_auth timeout was stricter than peer 2FA paths, which
block on pinentry indefinitely; replace it with u32::MAX so Ctrl+C is
the only escape, matching the rest. Drop the unconditional eager PIN
prompt: bridge UiCallback::request_pin into async pinentry on demand
via Handle::current().spawn + a sync channel under block_in_place, so
users whose keys have no client_pin skip the prompt entirely. Treat
webauthn() failures as fatal (propagate via ?) instead of looping into
unreachable!().
Restore the WebAuthn arms on header()/message() and look them up via
TwoFactorProviderType::WebAuthn so the user-facing strings live in one
place, like every other provider.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant