From 05c9af4d1e931b6d8cf3ed42a75998edb8b83c51 Mon Sep 17 00:00:00 2001 From: Raphael Roumezin <94066559+RaphaelRoumezin@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:30:32 +0200 Subject: [PATCH] fix: Allowed Chromium extensions as origins for passwordless login Co-authored-by: Keng <39216134+kengzzzz@users.noreply.github.com> --- src/api/core/mod.rs | 1 + src/api/core/two_factor/webauthn.rs | 10 +++---- src/api/core/webauthn.rs | 44 +++++++++++++++++++++++------ src/api/identity.rs | 8 +++--- 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 0518642a..c28ebae5 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -15,6 +15,7 @@ pub use ciphers::{CipherData, CipherSyncData, CipherSyncType, purge_trashed_ciph pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job}; pub use events::{event_cleanup_job, log_event, log_user_event}; pub use sends::purge_sends; +pub use webauthn::WEBAUTHN_PASSWORDLESS; use reqwest::Method; use rocket::{Catcher, Route, serde::json::Json, serde::json::Value}; diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index ba1a25fe..e6a6c516 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -27,7 +27,7 @@ use webauthn_rs_proto::{ RequestAuthenticationExtensions, UserVerificationPolicy, }; -pub static WEBAUTHN: LazyLock = LazyLock::new(|| { +pub static WEBAUTHN_2FA: LazyLock = LazyLock::new(|| { let domain = CONFIG.domain(); let domain_origin = CONFIG.domain_origin(); let rp_id = url::Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(); @@ -139,7 +139,7 @@ async fn generate_webauthn_challenge(data: Json, headers: Hea .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_2FA.start_passkey_registration( Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail &user.email, user.display_name(), @@ -272,7 +272,7 @@ async fn activate_webauthn(data: Json, headers: Headers, con }; // Verify the credentials with the saved state - let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?; + let credential = WEBAUTHN_2FA.finish_passkey_registration(&data.device_response.into(), &state)?; let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn).await?.1; // TODO: Check for repeated ID's @@ -382,7 +382,7 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonRes } // Generate a challenge based on the credentials - let (mut response, state) = WEBAUTHN.start_passkey_authentication(&creds)?; + let (mut response, state) = WEBAUTHN_2FA.start_passkey_authentication(&creds)?; // Modify to discourage user verification let mut state = serde_json::to_value(&state)?; @@ -437,7 +437,7 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &Db // Because of this we check the flag at runtime and update the registrations and state when needed let backup_flags_updated = check_and_update_backup_eligible(&rsp, &mut registrations, &mut state)?; - let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?; + let authentication_result = WEBAUTHN_2FA.finish_passkey_authentication(&rsp, &state)?; for reg in &mut registrations { if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) { diff --git a/src/api/core/webauthn.rs b/src/api/core/webauthn.rs index 6b6b1aa2..766efa3e 100644 --- a/src/api/core/webauthn.rs +++ b/src/api/core/webauthn.rs @@ -1,14 +1,16 @@ +use std::sync::LazyLock; + use rocket::{http::Status, serde::json::Json}; use serde_json::Value; -use webauthn_rs::prelude::{Passkey, PasskeyRegistration}; +use webauthn_rs::{ + Webauthn, WebauthnBuilder, + prelude::{Passkey, PasskeyRegistration}, +}; use webauthn_rs_proto::{COSEAlgorithm, UserVerificationPolicy, options::PubKeyCredParams}; use crate::{ CONFIG, - api::{ - ApiResult, JsonResult, PasswordOrOtpData, - core::two_factor::webauthn::{RegisterPublicKeyCredentialCopy, WEBAUTHN}, - }, + api::{ApiResult, JsonResult, PasswordOrOtpData, core::two_factor::webauthn::RegisterPublicKeyCredentialCopy}, auth::Headers, db::{ DbConn, @@ -16,6 +18,28 @@ use crate::{ }, }; +pub static WEBAUTHN_PASSWORDLESS: LazyLock = LazyLock::new(|| { + let domain = CONFIG.domain(); + let domain_origin = CONFIG.domain_origin(); + let rp_id = url::Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(); + let rp_origin = url::Url::parse(&domain_origin).unwrap(); + + let mut webauthn = WebauthnBuilder::new(&rp_id, &rp_origin) + .expect("Creating WebauthnBuilder failed") + .rp_name(&domain) + .timeout(tokio::time::Duration::from_mins(1)); + + for origin in [ + "chrome-extension://nngceckbapebfimnlniiiahkandclblb", // Chrome Web Store + "chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh", // Edge Add-ons + ] { + let origin = url::Url::parse(origin).expect("origin should parse"); + webauthn = webauthn.append_allowed_origin(&origin); + } + + webauthn.build().expect("Building Webauthn failed") +}); + pub fn routes() -> Vec { routes![get_webauthn, post_webauthn, post_webauthn_attestation_options, post_webauthn_delete] } @@ -81,8 +105,12 @@ async fn post_webauthn_attestation_options( let user_uuid = uuid::Uuid::parse_str(&user.uuid).expect("Failed to parse user UUID"); - let (mut challenge, state) = - WEBAUTHN.start_passkey_registration(user_uuid, &user.email, user.display_name(), Some(existing_cred_ids))?; + let (mut challenge, state) = WEBAUTHN_PASSWORDLESS.start_passkey_registration( + user_uuid, + &user.email, + user.display_name(), + Some(existing_cred_ids), + )?; // For passkey login, we need discoverable credentials (resident keys). // start_passkey_registration() defaults to require_resident_key=false, but passkey login @@ -192,7 +220,7 @@ async fn post_webauthn( Some(tf) => { let state: PasskeyRegistration = serde_json::from_str(&tf.data)?; tf.delete(&conn).await?; - WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)? + WEBAUTHN_PASSWORDLESS.finish_passkey_registration(&data.device_response.into(), &state)? } None => err!("No registration challenge found. Please try again."), }; diff --git a/src/api/identity.rs b/src/api/identity.rs index 3f7cc0cd..3d43801c 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -16,11 +16,11 @@ use crate::{ api::{ ApiResult, EmptyResult, JsonResult, core::{ + WEBAUTHN_PASSWORDLESS, accounts::{PreloginData, RegisterData, kdf_upgrade, prelogin, register}, log_user_event, two_factor::{ - authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, - webauthn::{self, WEBAUTHN}, + authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, yubikey, }, }, @@ -599,7 +599,7 @@ async fn webauthn_login(data: ConnectData, user_id: &mut Option, conn: & }; // Perform passkey authentication - let authentication_result = match WEBAUTHN.finish_passkey_authentication(&device_response, &state) { + let authentication_result = match WEBAUTHN_PASSWORDLESS.finish_passkey_authentication(&device_response, &state) { Ok(result) => result, Err(e) => { err!( @@ -1495,7 +1495,7 @@ fn get_webauthn_assertion_options() -> JsonResult { err!("Passkey login is not allowed") } - let (mut response, state) = WEBAUTHN.start_passkey_authentication(&[])?; + let (mut response, state) = WEBAUTHN_PASSWORDLESS.start_passkey_authentication(&[])?; // Allow any credential (discoverable) and require user verification response.public_key.allow_credentials = vec![];