Browse Source

Merge cdc43eab68 into 8e7eeab293

pull/6160/merge
Stefan Melmuk 4 days ago
committed by GitHub
parent
commit
1bedb7cc69
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 52
      src/api/core/two_factor/webauthn.rs
  2. 24
      src/api/identity.rs
  3. 1
      src/api/web.rs
  4. 4
      src/config.rs
  5. 2
      src/main.rs
  6. 7
      src/static/templates/scss/vaultwarden.scss.hbs

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

@ -17,7 +17,7 @@ use rocket::serde::json::Json;
use rocket::Route; use rocket::Route;
use serde_json::Value; use serde_json::Value;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, LazyLock}; use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
@ -29,7 +29,7 @@ use webauthn_rs_proto::{
RequestAuthenticationExtensions, RequestAuthenticationExtensions,
}; };
pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| { static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
let domain = CONFIG.domain(); let domain = CONFIG.domain();
let domain_origin = CONFIG.domain_origin(); 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(); let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default();
@ -41,11 +41,9 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
.timeout(Duration::from_millis(60000)) .timeout(Duration::from_millis(60000))
.danger_set_user_presence_only_security_keys(true); .danger_set_user_presence_only_security_keys(true);
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> { pub fn routes() -> Vec<Route> {
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,] routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
} }
@ -113,12 +111,7 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
} }
#[post("/two-factor/get-webauthn-challenge", data = "<data>")] #[post("/two-factor/get-webauthn-challenge", data = "<data>")]
async fn generate_webauthn_challenge( async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
data: Json<PasswordOrOtpData>,
headers: Headers,
webauthn: Webauthn2FaConfig<'_>,
mut conn: DbConn,
) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner(); let data: PasswordOrOtpData = data.into_inner();
let user = headers.user; let user = headers.user;
@ -131,7 +124,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 .map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
.collect(); .collect();
let (challenge, state) = webauthn.start_securitykey_registration( let (challenge, state) = WEBAUTHN.start_securitykey_registration(
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
&user.email, &user.email,
&user.name, &user.name,
@ -233,12 +226,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
} }
#[post("/two-factor/webauthn", data = "<data>")] #[post("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn( async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
data: Json<EnableWebauthnData>,
headers: Headers,
webauthn: Webauthn2FaConfig<'_>,
mut conn: DbConn,
) -> JsonResult {
let data: EnableWebauthnData = data.into_inner(); let data: EnableWebauthnData = data.into_inner();
let mut user = headers.user; let mut user = headers.user;
@ -261,7 +249,7 @@ async fn activate_webauthn(
}; };
// Verify the credentials with the saved state // Verify the credentials with the saved state
let credential = webauthn.finish_securitykey_registration(&data.device_response.into(), &state)?; let credential = WEBAUTHN.finish_securitykey_registration(&data.device_response.into(), &state)?;
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1; let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
// TODO: Check for repeated ID's // TODO: Check for repeated ID's
@ -290,13 +278,8 @@ async fn activate_webauthn(
} }
#[put("/two-factor/webauthn", data = "<data>")] #[put("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn_put( async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
data: Json<EnableWebauthnData>, activate_webauthn(data, headers, conn).await
headers: Headers,
webauthn: Webauthn2FaConfig<'_>,
conn: DbConn,
) -> JsonResult {
activate_webauthn(data, headers, webauthn, conn).await
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -366,11 +349,7 @@ pub async fn get_webauthn_registrations(
} }
} }
pub async fn generate_webauthn_login( pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
user_id: &UserId,
webauthn: Webauthn2FaConfig<'_>,
conn: &mut DbConn,
) -> JsonResult {
// Load saved credentials // Load saved credentials
let creds: Vec<_> = get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect(); let creds: Vec<_> = get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
@ -379,7 +358,7 @@ pub async fn generate_webauthn_login(
} }
// Generate a challenge based on the credentials // Generate a challenge based on the credentials
let (mut response, state) = webauthn.start_securitykey_authentication(&creds)?; let (mut response, state) = WEBAUTHN.start_securitykey_authentication(&creds)?;
// Modify to discourage user verification // Modify to discourage user verification
let mut state = serde_json::to_value(&state)?; let mut state = serde_json::to_value(&state)?;
@ -406,12 +385,7 @@ pub async fn generate_webauthn_login(
Ok(Json(serde_json::to_value(response.public_key)?)) Ok(Json(serde_json::to_value(response.public_key)?))
} }
pub async fn validate_webauthn_login( pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
user_id: &UserId,
response: &str,
webauthn: Webauthn2FaConfig<'_>,
conn: &mut DbConn,
) -> EmptyResult {
let type_ = TwoFactorType::WebauthnLoginChallenge as i32; let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await { let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
Some(tf) => { Some(tf) => {
@ -432,7 +406,7 @@ pub async fn validate_webauthn_login(
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1; let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
let authentication_result = webauthn.finish_securitykey_authentication(&rsp, &state)?; let authentication_result = WEBAUTHN.finish_securitykey_authentication(&rsp, &state)?;
for reg in &mut registrations { for reg in &mut registrations {
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) { if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {

24
src/api/identity.rs

@ -9,7 +9,6 @@ use rocket::{
}; };
use serde_json::Value; use serde_json::Value;
use crate::api::core::two_factor::webauthn::Webauthn2FaConfig;
use crate::{ use crate::{
api::{ api::{
core::{ core::{
@ -49,7 +48,6 @@ async fn login(
data: Form<ConnectData>, data: Form<ConnectData>,
client_header: ClientHeaders, client_header: ClientHeaders,
client_version: Option<ClientVersion>, client_version: Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
mut conn: DbConn, mut conn: DbConn,
) -> JsonResult { ) -> JsonResult {
let data: ConnectData = data.into_inner(); 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_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type 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" => { "client_credentials" => {
_check_is_some(&data.client_id, "client_id cannot be blank")?; _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_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type 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"), "authorization_code" => err!("SSO sign-in is not available"),
t => err!("Invalid type", t), t => err!("Invalid type", t),
@ -171,7 +169,6 @@ async fn _sso_login(
conn: &mut DbConn, conn: &mut DbConn,
ip: &ClientIp, ip: &ClientIp,
client_version: &Option<ClientVersion>, client_version: &Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
) -> JsonResult { ) -> JsonResult {
AuthMethod::Sso.check_scope(data.scope.as_ref())?; AuthMethod::Sso.check_scope(data.scope.as_ref())?;
@ -270,7 +267,7 @@ async fn _sso_login(
} }
Some((mut user, sso_user)) => { Some((mut user, sso_user)) => {
let mut device = get_device(&data, conn, &user).await?; 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() { if user.private_key.is_none() {
// User was invited a stub was created // User was invited a stub was created
@ -325,7 +322,6 @@ async fn _password_login(
conn: &mut DbConn, conn: &mut DbConn,
ip: &ClientIp, ip: &ClientIp,
client_version: &Option<ClientVersion>, client_version: &Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
) -> JsonResult { ) -> JsonResult {
// Validate scope // Validate scope
AuthMethod::Password.check_scope(data.scope.as_ref())?; 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 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); let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
@ -668,7 +664,6 @@ async fn twofactor_auth(
device: &mut Device, device: &mut Device,
ip: &ClientIp, ip: &ClientIp,
client_version: &Option<ClientVersion>, client_version: &Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
conn: &mut DbConn, conn: &mut DbConn,
) -> ApiResult<Option<String>> { ) -> ApiResult<Option<String>> {
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
@ -688,7 +683,7 @@ async fn twofactor_auth(
Some(ref code) => code, Some(ref code) => code,
None => { None => {
err_json!( 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" "2FA token not provided"
) )
} }
@ -705,9 +700,7 @@ async fn twofactor_auth(
Some(TwoFactorType::Authenticator) => { Some(TwoFactorType::Authenticator) => {
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await? authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
} }
Some(TwoFactorType::Webauthn) => { Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
webauthn::validate_webauthn_login(&user.uuid, twofactor_code, webauthn, conn).await?
}
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
Some(TwoFactorType::Duo) => { Some(TwoFactorType::Duo) => {
match CONFIG.duo_use_iframe() { match CONFIG.duo_use_iframe() {
@ -739,7 +732,7 @@ async fn twofactor_auth(
} }
_ => { _ => {
err_json!( 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" "2FA Remember token not provided"
) )
} }
@ -773,7 +766,6 @@ async fn _json_err_twofactor(
user_id: &UserId, user_id: &UserId,
data: &ConnectData, data: &ConnectData,
client_version: &Option<ClientVersion>, client_version: &Option<ClientVersion>,
webauthn: Webauthn2FaConfig<'_>,
conn: &mut DbConn, conn: &mut DbConn,
) -> ApiResult<Value> { ) -> ApiResult<Value> {
let mut result = json!({ let mut result = json!({
@ -793,7 +785,7 @@ async fn _json_err_twofactor(
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { 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; result["TwoFactorProviders2"][provider.to_string()] = request.0;
} }

1
src/api/web.rs

@ -64,6 +64,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
"sso_enabled": CONFIG.sso_enabled(), "sso_enabled": CONFIG.sso_enabled(),
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(), "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(), "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) { let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {

4
src/config.rs

@ -1504,6 +1504,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. /// Tests whether the admin token is set to a non-empty value.
pub fn is_admin_token_set(&self) -> bool { pub fn is_admin_token_set(&self) -> bool {
let token = self.admin_token(); let token = self.admin_token();

2
src/main.rs

@ -61,7 +61,6 @@ mod sso_client;
mod util; mod util;
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts; 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::purge_auth_requests;
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS}; use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
pub use config::{PathType, CONFIG}; pub use config::{PathType, CONFIG};
@ -601,7 +600,6 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
.manage(pool) .manage(pool)
.manage(Arc::clone(&WS_USERS)) .manage(Arc::clone(&WS_USERS))
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS)) .manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
.manage(Arc::clone(&WEBAUTHN_2FA_CONFIG))
.attach(util::AppHeaders()) .attach(util::AppHeaders())
.attach(util::Cors()) .attach(util::Cors())
.attach(util::BetterLogging(extra_debug)) .attach(util::BetterLogging(extra_debug))

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

@ -168,6 +168,13 @@ app-root a[routerlink="/signup"] {
} }
{{/unless}} {{/unless}}
{{#unless webauthn_2fa_supported}}
/* Hide `Passkey` 2FA if it is not supported */
.providers-2fa-7 {
@extend %vw-hide;
}
{{/unless}}
{{#unless emergency_access_allowed}} {{#unless emergency_access_allowed}}
/* Hide Emergency Access if not allowed */ /* Hide Emergency Access if not allowed */
bit-nav-item[route="settings/emergency-access"] { bit-nav-item[route="settings/emergency-access"] {

Loading…
Cancel
Save