Skip to content

Commit 5a8736e

Browse files
authored
make webauthn more optional (#6160)
* make webauthn optional * hide passkey if domain is not set
1 parent f76362f commit 5a8736e

File tree

6 files changed

+33
-57
lines changed

6 files changed

+33
-57
lines changed

src/api/core/two_factor/webauthn.rs

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use rocket::serde::json::Json;
1717
use rocket::Route;
1818
use serde_json::Value;
1919
use std::str::FromStr;
20-
use std::sync::{Arc, LazyLock};
20+
use std::sync::LazyLock;
2121
use std::time::Duration;
2222
use url::Url;
2323
use uuid::Uuid;
@@ -29,7 +29,7 @@ use webauthn_rs_proto::{
2929
RequestAuthenticationExtensions, UserVerificationPolicy,
3030
};
3131

32-
pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
32+
static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
3333
let domain = CONFIG.domain();
3434
let domain_origin = CONFIG.domain_origin();
3535
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(|| {
4040
.rp_name(&domain)
4141
.timeout(Duration::from_millis(60000));
4242

43-
Arc::new(webauthn.build().expect("Building Webauthn failed"))
43+
webauthn.build().expect("Building Webauthn failed")
4444
});
4545

46-
pub type Webauthn2FaConfig<'a> = &'a rocket::State<Arc<Webauthn>>;
47-
4846
pub fn routes() -> Vec<Route> {
4947
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
5048
}
@@ -130,12 +128,7 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
130128
}
131129

132130
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
133-
async fn generate_webauthn_challenge(
134-
data: Json<PasswordOrOtpData>,
135-
headers: Headers,
136-
webauthn: Webauthn2FaConfig<'_>,
137-
mut conn: DbConn,
138-
) -> JsonResult {
131+
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
139132
let data: PasswordOrOtpData = data.into_inner();
140133
let user = headers.user;
141134

@@ -148,7 +141,7 @@ async fn generate_webauthn_challenge(
148141
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
149142
.collect();
150143

151-
let (mut challenge, state) = webauthn.start_passkey_registration(
144+
let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
152145
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
153146
&user.email,
154147
&user.name,
@@ -259,12 +252,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
259252
}
260253

261254
#[post("/two-factor/webauthn", data = "<data>")]
262-
async fn activate_webauthn(
263-
data: Json<EnableWebauthnData>,
264-
headers: Headers,
265-
webauthn: Webauthn2FaConfig<'_>,
266-
mut conn: DbConn,
267-
) -> JsonResult {
255+
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
268256
let data: EnableWebauthnData = data.into_inner();
269257
let mut user = headers.user;
270258

@@ -287,7 +275,7 @@ async fn activate_webauthn(
287275
};
288276

289277
// Verify the credentials with the saved state
290-
let credential = webauthn.finish_passkey_registration(&data.device_response.into(), &state)?;
278+
let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?;
291279

292280
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
293281
// TODO: Check for repeated ID's
@@ -316,13 +304,8 @@ async fn activate_webauthn(
316304
}
317305

318306
#[put("/two-factor/webauthn", data = "<data>")]
319-
async fn activate_webauthn_put(
320-
data: Json<EnableWebauthnData>,
321-
headers: Headers,
322-
webauthn: Webauthn2FaConfig<'_>,
323-
conn: DbConn,
324-
) -> JsonResult {
325-
activate_webauthn(data, headers, webauthn, conn).await
307+
async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
308+
activate_webauthn(data, headers, conn).await
326309
}
327310

328311
#[derive(Debug, Deserialize)]
@@ -392,11 +375,7 @@ pub async fn get_webauthn_registrations(
392375
}
393376
}
394377

395-
pub async fn generate_webauthn_login(
396-
user_id: &UserId,
397-
webauthn: Webauthn2FaConfig<'_>,
398-
conn: &mut DbConn,
399-
) -> JsonResult {
378+
pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
400379
// Load saved credentials
401380
let creds: Vec<Passkey> =
402381
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(
406385
}
407386

408387
// Generate a challenge based on the credentials
409-
let (mut response, state) = webauthn.start_passkey_authentication(&creds)?;
388+
let (mut response, state) = WEBAUTHN.start_passkey_authentication(&creds)?;
410389

411390
// Modify to discourage user verification
412391
let mut state = serde_json::to_value(&state)?;
@@ -436,12 +415,7 @@ pub async fn generate_webauthn_login(
436415
Ok(Json(serde_json::to_value(response.public_key)?))
437416
}
438417

439-
pub async fn validate_webauthn_login(
440-
user_id: &UserId,
441-
response: &str,
442-
webauthn: Webauthn2FaConfig<'_>,
443-
conn: &mut DbConn,
444-
) -> EmptyResult {
418+
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
445419
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
446420
let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
447421
Some(tf) => {
@@ -467,7 +441,7 @@ pub async fn validate_webauthn_login(
467441
// Because of this we check the flag at runtime and update the registrations and state when needed
468442
check_and_update_backup_eligible(user_id, &rsp, &mut registrations, &mut state, conn).await?;
469443

470-
let authentication_result = webauthn.finish_passkey_authentication(&rsp, &state)?;
444+
let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?;
471445

472446
for reg in &mut registrations {
473447
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {

src/api/identity.rs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use rocket::{
99
};
1010
use serde_json::Value;
1111

12-
use crate::api::core::two_factor::webauthn::Webauthn2FaConfig;
1312
use crate::{
1413
api::{
1514
core::{
@@ -49,7 +48,6 @@ async fn login(
4948
data: Form<ConnectData>,
5049
client_header: ClientHeaders,
5150
client_version: Option<ClientVersion>,
52-
webauthn: Webauthn2FaConfig<'_>,
5351
mut conn: DbConn,
5452
) -> JsonResult {
5553
let data: ConnectData = data.into_inner();
@@ -72,7 +70,7 @@ async fn login(
7270
_check_is_some(&data.device_name, "device_name cannot be blank")?;
7371
_check_is_some(&data.device_type, "device_type cannot be blank")?;
7472

75-
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await
73+
_password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
7674
}
7775
"client_credentials" => {
7876
_check_is_some(&data.client_id, "client_id cannot be blank")?;
@@ -93,7 +91,7 @@ async fn login(
9391
_check_is_some(&data.device_name, "device_name cannot be blank")?;
9492
_check_is_some(&data.device_type, "device_type cannot be blank")?;
9593

96-
_sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await
94+
_sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await
9795
}
9896
"authorization_code" => err!("SSO sign-in is not available"),
9997
t => err!("Invalid type", t),
@@ -171,7 +169,6 @@ async fn _sso_login(
171169
conn: &mut DbConn,
172170
ip: &ClientIp,
173171
client_version: &Option<ClientVersion>,
174-
webauthn: Webauthn2FaConfig<'_>,
175172
) -> JsonResult {
176173
AuthMethod::Sso.check_scope(data.scope.as_ref())?;
177174

@@ -270,7 +267,7 @@ async fn _sso_login(
270267
}
271268
Some((mut user, sso_user)) => {
272269
let mut device = get_device(&data, conn, &user).await?;
273-
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?;
270+
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?;
274271

275272
if user.private_key.is_none() {
276273
// User was invited a stub was created
@@ -325,7 +322,6 @@ async fn _password_login(
325322
conn: &mut DbConn,
326323
ip: &ClientIp,
327324
client_version: &Option<ClientVersion>,
328-
webauthn: Webauthn2FaConfig<'_>,
329325
) -> JsonResult {
330326
// Validate scope
331327
AuthMethod::Password.check_scope(data.scope.as_ref())?;
@@ -435,7 +431,7 @@ async fn _password_login(
435431

436432
let mut device = get_device(&data, conn, &user).await?;
437433

438-
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?;
434+
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?;
439435

440436
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
441437

@@ -667,7 +663,6 @@ async fn twofactor_auth(
667663
device: &mut Device,
668664
ip: &ClientIp,
669665
client_version: &Option<ClientVersion>,
670-
webauthn: Webauthn2FaConfig<'_>,
671666
conn: &mut DbConn,
672667
) -> ApiResult<Option<String>> {
673668
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
@@ -687,7 +682,7 @@ async fn twofactor_auth(
687682
Some(ref code) => code,
688683
None => {
689684
err_json!(
690-
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?,
685+
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
691686
"2FA token not provided"
692687
)
693688
}
@@ -704,9 +699,7 @@ async fn twofactor_auth(
704699
Some(TwoFactorType::Authenticator) => {
705700
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
706701
}
707-
Some(TwoFactorType::Webauthn) => {
708-
webauthn::validate_webauthn_login(&user.uuid, twofactor_code, webauthn, conn).await?
709-
}
702+
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
710703
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
711704
Some(TwoFactorType::Duo) => {
712705
match CONFIG.duo_use_iframe() {
@@ -738,7 +731,7 @@ async fn twofactor_auth(
738731
}
739732
_ => {
740733
err_json!(
741-
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?,
734+
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
742735
"2FA Remember token not provided"
743736
)
744737
}
@@ -772,7 +765,6 @@ async fn _json_err_twofactor(
772765
user_id: &UserId,
773766
data: &ConnectData,
774767
client_version: &Option<ClientVersion>,
775-
webauthn: Webauthn2FaConfig<'_>,
776768
conn: &mut DbConn,
777769
) -> ApiResult<Value> {
778770
let mut result = json!({
@@ -792,7 +784,7 @@ async fn _json_err_twofactor(
792784
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
793785

794786
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
795-
let request = webauthn::generate_webauthn_login(user_id, webauthn, conn).await?;
787+
let request = webauthn::generate_webauthn_login(user_id, conn).await?;
796788
result["TwoFactorProviders2"][provider.to_string()] = request.0;
797789
}
798790

src/api/web.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
6464
"sso_enabled": CONFIG.sso_enabled(),
6565
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(),
6666
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
67+
"webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(),
6768
});
6869

6970
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,10 @@ impl Config {
15251525
}
15261526
}
15271527

1528+
pub fn is_webauthn_2fa_supported(&self) -> bool {
1529+
Url::parse(&self.domain()).expect("DOMAIN not a valid URL").domain().is_some()
1530+
}
1531+
15281532
/// Tests whether the admin token is set to a non-empty value.
15291533
pub fn is_admin_token_set(&self) -> bool {
15301534
let token = self.admin_token();

src/main.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ mod sso_client;
6161
mod util;
6262

6363
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
64-
use crate::api::core::two_factor::webauthn::WEBAUTHN_2FA_CONFIG;
6564
use crate::api::purge_auth_requests;
6665
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
6766
pub use config::{PathType, CONFIG};
@@ -601,7 +600,6 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
601600
.manage(pool)
602601
.manage(Arc::clone(&WS_USERS))
603602
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
604-
.manage(Arc::clone(&WEBAUTHN_2FA_CONFIG))
605603
.attach(util::AppHeaders())
606604
.attach(util::Cors())
607605
.attach(util::BetterLogging(extra_debug))

src/static/templates/scss/vaultwarden.scss.hbs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,13 @@ app-root a[routerlink="/signup"] {
172172
}
173173
{{/unless}}
174174

175+
{{#unless webauthn_2fa_supported}}
176+
/* Hide `Passkey` 2FA if it is not supported */
177+
.providers-2fa-7 {
178+
@extend %vw-hide;
179+
}
180+
{{/unless}}
181+
175182
{{#unless emergency_access_allowed}}
176183
/* Hide Emergency Access if not allowed */
177184
bit-nav-item[route="settings/emergency-access"] {

0 commit comments

Comments
 (0)