From 8cac5c219d0873dc2b0324c31644066eb31496ee Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sun, 10 Aug 2025 00:41:42 +0200 Subject: [PATCH 1/2] make webauthn optional --- src/api/core/two_factor/webauthn.rs | 52 ++++++++--------------------- src/api/identity.rs | 24 +++++-------- src/main.rs | 2 -- 3 files changed, 21 insertions(+), 57 deletions(-) diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 049145e0..517867b8 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -17,7 +17,7 @@ use rocket::serde::json::Json; use rocket::Route; use serde_json::Value; use std::str::FromStr; -use std::sync::{Arc, LazyLock}; +use std::sync::LazyLock; use std::time::Duration; use url::Url; use uuid::Uuid; @@ -29,7 +29,7 @@ use webauthn_rs_proto::{ RequestAuthenticationExtensions, }; -pub static WEBAUTHN_2FA_CONFIG: LazyLock> = LazyLock::new(|| { +static WEBAUTHN: LazyLock = LazyLock::new(|| { let domain = CONFIG.domain(); let domain_origin = CONFIG.domain_origin(); let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(); @@ -41,11 +41,9 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock> = LazyLock::new(|| { .timeout(Duration::from_millis(60000)) .danger_set_user_presence_only_security_keys(true); - Arc::new(webauthn.build().expect("Building Webauthn failed")) + webauthn.build().expect("Building Webauthn failed") }); -pub type Webauthn2FaConfig<'a> = &'a rocket::State>; - pub fn routes() -> Vec { routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,] } @@ -113,12 +111,7 @@ async fn get_webauthn(data: Json, headers: Headers, mut conn: } #[post("/two-factor/get-webauthn-challenge", data = "")] -async fn generate_webauthn_challenge( - data: Json, - headers: Headers, - webauthn: Webauthn2FaConfig<'_>, - mut conn: DbConn, -) -> JsonResult { +async fn generate_webauthn_challenge(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; @@ -131,7 +124,7 @@ async fn generate_webauthn_challenge( .map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering .collect(); - let (challenge, state) = webauthn.start_securitykey_registration( + let (challenge, state) = WEBAUTHN.start_securitykey_registration( Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail &user.email, &user.name, @@ -233,12 +226,7 @@ impl From for PublicKeyCredential { } #[post("/two-factor/webauthn", data = "")] -async fn activate_webauthn( - data: Json, - headers: Headers, - webauthn: Webauthn2FaConfig<'_>, - mut conn: DbConn, -) -> JsonResult { +async fn activate_webauthn(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { let data: EnableWebauthnData = data.into_inner(); let mut user = headers.user; @@ -261,7 +249,7 @@ async fn activate_webauthn( }; // Verify the credentials with the saved state - let credential = webauthn.finish_securitykey_registration(&data.device_response.into(), &state)?; + let credential = WEBAUTHN.finish_securitykey_registration(&data.device_response.into(), &state)?; let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1; // TODO: Check for repeated ID's @@ -290,13 +278,8 @@ async fn activate_webauthn( } #[put("/two-factor/webauthn", data = "")] -async fn activate_webauthn_put( - data: Json, - headers: Headers, - webauthn: Webauthn2FaConfig<'_>, - conn: DbConn, -) -> JsonResult { - activate_webauthn(data, headers, webauthn, conn).await +async fn activate_webauthn_put(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + activate_webauthn(data, headers, conn).await } #[derive(Debug, Deserialize)] @@ -366,11 +349,7 @@ pub async fn get_webauthn_registrations( } } -pub async fn generate_webauthn_login( - user_id: &UserId, - webauthn: Webauthn2FaConfig<'_>, - conn: &mut DbConn, -) -> JsonResult { +pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult { // Load saved credentials let creds: Vec<_> = get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect(); @@ -379,7 +358,7 @@ pub async fn generate_webauthn_login( } // Generate a challenge based on the credentials - let (mut response, state) = webauthn.start_securitykey_authentication(&creds)?; + let (mut response, state) = WEBAUTHN.start_securitykey_authentication(&creds)?; // Modify to discourage user verification let mut state = serde_json::to_value(&state)?; @@ -406,12 +385,7 @@ pub async fn generate_webauthn_login( Ok(Json(serde_json::to_value(response.public_key)?)) } -pub async fn validate_webauthn_login( - user_id: &UserId, - response: &str, - webauthn: Webauthn2FaConfig<'_>, - conn: &mut DbConn, -) -> EmptyResult { +pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult { let type_ = TwoFactorType::WebauthnLoginChallenge as i32; let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await { Some(tf) => { @@ -432,7 +406,7 @@ pub async fn validate_webauthn_login( let mut registrations = get_webauthn_registrations(user_id, conn).await?.1; - let authentication_result = webauthn.finish_securitykey_authentication(&rsp, &state)?; + let authentication_result = WEBAUTHN.finish_securitykey_authentication(&rsp, &state)?; for reg in &mut registrations { if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) { diff --git a/src/api/identity.rs b/src/api/identity.rs index ba22104e..476b286f 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -9,7 +9,6 @@ use rocket::{ }; use serde_json::Value; -use crate::api::core::two_factor::webauthn::Webauthn2FaConfig; use crate::{ api::{ core::{ @@ -49,7 +48,6 @@ async fn login( data: Form, client_header: ClientHeaders, client_version: Option, - webauthn: Webauthn2FaConfig<'_>, mut conn: DbConn, ) -> JsonResult { let data: ConnectData = data.into_inner(); @@ -72,7 +70,7 @@ async fn login( _check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?; - _password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await + _password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await } "client_credentials" => { _check_is_some(&data.client_id, "client_id cannot be blank")?; @@ -93,7 +91,7 @@ async fn login( _check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?; - _sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await + _sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await } "authorization_code" => err!("SSO sign-in is not available"), t => err!("Invalid type", t), @@ -171,7 +169,6 @@ async fn _sso_login( conn: &mut DbConn, ip: &ClientIp, client_version: &Option, - webauthn: Webauthn2FaConfig<'_>, ) -> JsonResult { AuthMethod::Sso.check_scope(data.scope.as_ref())?; @@ -270,7 +267,7 @@ async fn _sso_login( } Some((mut user, sso_user)) => { let mut device = get_device(&data, conn, &user).await?; - let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?; + let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?; if user.private_key.is_none() { // User was invited a stub was created @@ -325,7 +322,6 @@ async fn _password_login( conn: &mut DbConn, ip: &ClientIp, client_version: &Option, - webauthn: Webauthn2FaConfig<'_>, ) -> JsonResult { // Validate scope AuthMethod::Password.check_scope(data.scope.as_ref())?; @@ -435,7 +431,7 @@ async fn _password_login( let mut device = get_device(&data, conn, &user).await?; - let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?; + let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?; let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id); @@ -668,7 +664,6 @@ async fn twofactor_auth( device: &mut Device, ip: &ClientIp, client_version: &Option, - webauthn: Webauthn2FaConfig<'_>, conn: &mut DbConn, ) -> ApiResult> { let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; @@ -688,7 +683,7 @@ async fn twofactor_auth( Some(ref code) => code, None => { err_json!( - _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?, + _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, "2FA token not provided" ) } @@ -705,9 +700,7 @@ async fn twofactor_auth( Some(TwoFactorType::Authenticator) => { authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await? } - Some(TwoFactorType::Webauthn) => { - webauthn::validate_webauthn_login(&user.uuid, twofactor_code, webauthn, conn).await? - } + Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?, Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::Duo) => { match CONFIG.duo_use_iframe() { @@ -739,7 +732,7 @@ async fn twofactor_auth( } _ => { err_json!( - _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?, + _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, "2FA Remember token not provided" ) } @@ -773,7 +766,6 @@ async fn _json_err_twofactor( user_id: &UserId, data: &ConnectData, client_version: &Option, - webauthn: Webauthn2FaConfig<'_>, conn: &mut DbConn, ) -> ApiResult { let mut result = json!({ @@ -793,7 +785,7 @@ async fn _json_err_twofactor( Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { - let request = webauthn::generate_webauthn_login(user_id, webauthn, conn).await?; + let request = webauthn::generate_webauthn_login(user_id, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; } diff --git a/src/main.rs b/src/main.rs index e91dcbc4..3195300b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,7 +61,6 @@ mod sso_client; mod util; use crate::api::core::two_factor::duo_oidc::purge_duo_contexts; -use crate::api::core::two_factor::webauthn::WEBAUTHN_2FA_CONFIG; use crate::api::purge_auth_requests; use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS}; pub use config::{PathType, CONFIG}; @@ -601,7 +600,6 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> .manage(pool) .manage(Arc::clone(&WS_USERS)) .manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS)) - .manage(Arc::clone(&WEBAUTHN_2FA_CONFIG)) .attach(util::AppHeaders()) .attach(util::Cors()) .attach(util::BetterLogging(extra_debug)) From cdc43eab681ac77aaddc3a0a0be7b298d2f7e42d Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sun, 10 Aug 2025 01:17:01 +0200 Subject: [PATCH 2/2] hide passkey if domain is not set --- src/api/web.rs | 1 + src/config.rs | 4 ++++ src/static/templates/scss/vaultwarden.scss.hbs | 7 +++++++ 3 files changed, 12 insertions(+) diff --git a/src/api/web.rs b/src/api/web.rs index d8e35009..98d51a5e 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -64,6 +64,7 @@ fn vaultwarden_css() -> Cached> { "sso_enabled": CONFIG.sso_enabled(), "sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(), "yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(), + "webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(), }); let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) { diff --git a/src/config.rs b/src/config.rs index 545d7dce..91f7fe42 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1504,6 +1504,10 @@ impl Config { } } + pub fn is_webauthn_2fa_supported(&self) -> bool { + Url::parse(&self.domain()).expect("DOMAIN not a valid URL").domain().is_some() + } + /// Tests whether the admin token is set to a non-empty value. pub fn is_admin_token_set(&self) -> bool { let token = self.admin_token(); diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index b031404d..c1c5eec1 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -168,6 +168,13 @@ app-root a[routerlink="/signup"] { } {{/unless}} +{{#unless webauthn_2fa_supported}} +/* Hide `Passkey` 2FA if it is not supported */ +.providers-2fa-7 { + @extend %vw-hide; +} +{{/unless}} + {{#unless emergency_access_allowed}} /* Hide Emergency Access if not allowed */ bit-nav-item[route="settings/emergency-access"] {