From bbdc52438c6fc9cb569f2e7b2807d7727df9ca39 Mon Sep 17 00:00:00 2001 From: 0x484558 <0x484558@pm.me> Date: Tue, 24 Mar 2026 23:39:40 +0100 Subject: [PATCH 1/2] do not display unavailable 2FA options --- src/api/core/two_factor/mod.rs | 40 ++++++++++++++++++++++++++++++++-- src/api/identity.rs | 20 +++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 34fbfaa9..0f93ab62 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -2,6 +2,7 @@ use chrono::{TimeDelta, Utc}; use data_encoding::BASE32; use rocket::serde::json::Json; use rocket::Route; +use serde::Deserialize; use serde_json::Value; use crate::{ @@ -14,7 +15,7 @@ use crate::{ db::{ models::{ DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor, - TwoFactorIncomplete, User, UserId, + TwoFactorIncomplete, TwoFactorType, User, UserId, }, DbConn, DbPool, }, @@ -31,6 +32,37 @@ pub mod protected_actions; pub mod webauthn; pub mod yubikey; +fn has_global_duo_credentials() -> bool { + CONFIG._enable_duo() && CONFIG.duo_host().is_some() && CONFIG.duo_ikey().is_some() && CONFIG.duo_skey().is_some() +} + +pub fn is_twofactor_provider_usable(provider_type: i32, provider_data: Option<&str>) -> bool { + #[derive(Deserialize)] + struct DuoProviderData { + host: String, + ik: String, + sk: String, + } + + match provider_type { + x if x == TwoFactorType::Authenticator as i32 => true, + x if x == TwoFactorType::Email as i32 => CONFIG._enable_email_2fa(), + x if x == TwoFactorType::Duo as i32 || x == TwoFactorType::OrganizationDuo as i32 => { + provider_data + .and_then(|raw| serde_json::from_str::(raw).ok()) + .is_some_and(|duo| !duo.host.is_empty() && !duo.ik.is_empty() && !duo.sk.is_empty()) + || has_global_duo_credentials() + } + x if x == TwoFactorType::YubiKey as i32 => { + CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some() + } + x if x == TwoFactorType::Webauthn as i32 => CONFIG.domain_set(), + x if x == TwoFactorType::Remember as i32 => !CONFIG.disable_2fa_remember(), + x if x == TwoFactorType::RecoveryCode as i32 => true, + _ => false, + } +} + pub fn routes() -> Vec { let mut routes = routes![ get_twofactor, @@ -53,7 +85,11 @@ pub fn routes() -> Vec { #[get("/two-factor")] async fn get_twofactor(headers: Headers, conn: DbConn) -> Json { let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await; - let twofactors_json: Vec = twofactors.iter().map(TwoFactor::to_json_provider).collect(); + let twofactors_json: Vec = twofactors + .iter() + .filter(|tf| is_twofactor_provider_usable(tf.atype, Some(&tf.data))) + .map(TwoFactor::to_json_provider) + .collect(); Json(json!({ "data": twofactors_json, diff --git a/src/api/identity.rs b/src/api/identity.rs index fcd8c388..261ca708 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -14,7 +14,9 @@ use crate::{ core::{ accounts::{PreloginData, RegisterData, _prelogin, _register, kdf_upgrade}, log_user_event, - two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey}, + two_factor::{ + authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, yubikey, + }, }, master_password_policy, push::register_push_device, @@ -739,8 +741,22 @@ async fn twofactor_auth( TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?; - let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect(); + let twofactor_ids: Vec<_> = twofactors + .iter() + .filter(|tf| tf.enabled && is_twofactor_provider_usable(tf.atype, Some(&tf.data))) + .map(|tf| tf.atype) + .collect(); + if twofactor_ids.is_empty() { + err!("No enabled and usable two factor providers are available for this account") + } + let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one + if !twofactor_ids.contains(&selected_id) { + err_json!( + _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, + "Invalid two factor provider" + ) + } let twofactor_code = match data.two_factor_token { Some(ref code) => code, From 3c87f9cb28b6233acf766402d7946fcb4efdd623 Mon Sep 17 00:00:00 2001 From: 0x484558 <0x484558@pm.me> Date: Wed, 25 Mar 2026 22:00:15 +0100 Subject: [PATCH 2/2] use existing function to check webauthn support --- src/api/core/two_factor/mod.rs | 2 +- src/api/core/two_factor/webauthn.rs | 4 ++-- src/api/identity.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 0f93ab62..eb17a8b1 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -56,7 +56,7 @@ pub fn is_twofactor_provider_usable(provider_type: i32, provider_data: Option<&s x if x == TwoFactorType::YubiKey as i32 => { CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some() } - x if x == TwoFactorType::Webauthn as i32 => CONFIG.domain_set(), + x if x == TwoFactorType::Webauthn as i32 => CONFIG.is_webauthn_2fa_supported(), x if x == TwoFactorType::Remember as i32 => !CONFIG.disable_2fa_remember(), x if x == TwoFactorType::RecoveryCode as i32 => true, _ => false, diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 6ae12752..0ec0e30e 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -108,8 +108,8 @@ impl WebauthnRegistration { #[post("/two-factor/get-webauthn", data = "")] async fn get_webauthn(data: Json, headers: Headers, conn: DbConn) -> JsonResult { - if !CONFIG.domain_set() { - err!("`DOMAIN` environment variable is not set. Webauthn disabled") + if !CONFIG.is_webauthn_2fa_supported() { + err!("Configured `DOMAIN` is not compatible with Webauthn") } let data: PasswordOrOtpData = data.into_inner(); diff --git a/src/api/identity.rs b/src/api/identity.rs index 261ca708..10045d3d 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -887,7 +887,7 @@ async fn _json_err_twofactor( match TwoFactorType::from_i32(*provider) { Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } - Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { + Some(TwoFactorType::Webauthn) if CONFIG.is_webauthn_2fa_supported() => { let request = webauthn::generate_webauthn_login(user_id, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; }