Browse Source

fix: Allowed Chromium extensions as origins for passwordless login

Co-authored-by: Keng <39216134+kengzzzz@users.noreply.github.com>
pull/7370/head
Raphael Roumezin 4 weeks ago
parent
commit
05c9af4d1e
No known key found for this signature in database GPG Key ID: 91D7A94FE35B7922
  1. 1
      src/api/core/mod.rs
  2. 10
      src/api/core/two_factor/webauthn.rs
  3. 44
      src/api/core/webauthn.rs
  4. 8
      src/api/identity.rs

1
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};

10
src/api/core/two_factor/webauthn.rs

@ -27,7 +27,7 @@ use webauthn_rs_proto::{
RequestAuthenticationExtensions, UserVerificationPolicy,
};
pub static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
pub static WEBAUTHN_2FA: LazyLock<Webauthn> = 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<PasswordOrOtpData>, 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<EnableWebauthnData>, 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()) {

44
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<Webauthn> = 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<rocket::Route> {
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."),
};

8
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<UserId>, 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![];

Loading…
Cancel
Save