Browse Source

make webauthn more optional (#6160)

* make webauthn optional

* hide passkey if domain is not set
Stefan Melmuk 1 month ago
parent
commit
5a8736e116

+ 13 - 39
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, UserVerificationPolicy,
 };
 
-pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
+static WEBAUTHN: LazyLock<Webauthn> = 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();
@@ -40,11 +40,9 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
         .rp_name(&domain)
         .timeout(Duration::from_millis(60000));
 
-    Arc::new(webauthn.build().expect("Building Webauthn failed"))
+    webauthn.build().expect("Building Webauthn failed")
 });
 
-pub type Webauthn2FaConfig<'a> = &'a rocket::State<Arc<Webauthn>>;
-
 pub fn routes() -> Vec<Route> {
     routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
 }
@@ -130,12 +128,7 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
 }
 
 #[post("/two-factor/get-webauthn-challenge", data = "<data>")]
-async fn generate_webauthn_challenge(
-    data: Json<PasswordOrOtpData>,
-    headers: Headers,
-    webauthn: Webauthn2FaConfig<'_>,
-    mut conn: DbConn,
-) -> JsonResult {
+async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
     let data: PasswordOrOtpData = data.into_inner();
     let user = headers.user;
 
@@ -148,7 +141,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 (mut challenge, state) = webauthn.start_passkey_registration(
+    let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
         Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
         &user.email,
         &user.name,
@@ -259,12 +252,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
 }
 
 #[post("/two-factor/webauthn", data = "<data>")]
-async fn activate_webauthn(
-    data: Json<EnableWebauthnData>,
-    headers: Headers,
-    webauthn: Webauthn2FaConfig<'_>,
-    mut conn: DbConn,
-) -> JsonResult {
+async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
     let data: EnableWebauthnData = data.into_inner();
     let mut user = headers.user;
 
@@ -287,7 +275,7 @@ async fn activate_webauthn(
     };
 
     // Verify the credentials with the saved state
-    let credential = webauthn.finish_passkey_registration(&data.device_response.into(), &state)?;
+    let credential = WEBAUTHN.finish_passkey_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
@@ -316,13 +304,8 @@ async fn activate_webauthn(
 }
 
 #[put("/two-factor/webauthn", data = "<data>")]
-async fn activate_webauthn_put(
-    data: Json<EnableWebauthnData>,
-    headers: Headers,
-    webauthn: Webauthn2FaConfig<'_>,
-    conn: DbConn,
-) -> JsonResult {
-    activate_webauthn(data, headers, webauthn, conn).await
+async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_webauthn(data, headers, conn).await
 }
 
 #[derive(Debug, Deserialize)]
@@ -392,11 +375,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<Passkey> =
         get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
@@ -406,7 +385,7 @@ pub async fn generate_webauthn_login(
     }
 
     // Generate a challenge based on the credentials
-    let (mut response, state) = webauthn.start_passkey_authentication(&creds)?;
+    let (mut response, state) = WEBAUTHN.start_passkey_authentication(&creds)?;
 
     // Modify to discourage user verification
     let mut state = serde_json::to_value(&state)?;
@@ -436,12 +415,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 mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
         Some(tf) => {
@@ -467,7 +441,7 @@ pub async fn validate_webauthn_login(
     // Because of this we check the flag at runtime and update the registrations and state when needed
     check_and_update_backup_eligible(user_id, &rsp, &mut registrations, &mut state, conn).await?;
 
-    let authentication_result = webauthn.finish_passkey_authentication(&rsp, &state)?;
+    let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?;
 
     for reg in &mut registrations {
         if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {

+ 8 - 16
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<ConnectData>,
     client_header: ClientHeaders,
     client_version: Option<ClientVersion>,
-    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<ClientVersion>,
-    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<ClientVersion>,
-    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);
 
@@ -667,7 +663,6 @@ async fn twofactor_auth(
     device: &mut Device,
     ip: &ClientIp,
     client_version: &Option<ClientVersion>,
-    webauthn: Webauthn2FaConfig<'_>,
     conn: &mut DbConn,
 ) -> ApiResult<Option<String>> {
     let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
@@ -687,7 +682,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"
             )
         }
@@ -704,9 +699,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() {
@@ -738,7 +731,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"
                     )
                 }
@@ -772,7 +765,6 @@ async fn _json_err_twofactor(
     user_id: &UserId,
     data: &ConnectData,
     client_version: &Option<ClientVersion>,
-    webauthn: Webauthn2FaConfig<'_>,
     conn: &mut DbConn,
 ) -> ApiResult<Value> {
     let mut result = json!({
@@ -792,7 +784,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;
             }
 

+ 1 - 0
src/api/web.rs

@@ -64,6 +64,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
         "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) {

+ 4 - 0
src/config.rs

@@ -1525,6 +1525,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();

+ 0 - 2
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))

+ 7 - 0
src/static/templates/scss/vaultwarden.scss.hbs

@@ -172,6 +172,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"] {