mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-08-30 06:24:47 +00:00
make webauthn more optional (#6160)
* make webauthn optional * hide passkey if domain is not set
This commit is contained in:
parent
f76362ff89
commit
5a8736e116
6 changed files with 33 additions and 57 deletions
|
@ -17,7 +17,7 @@ use rocket::serde::json::Json;
|
|||
use rocket::Route;
|
||||
use serde_json::Value;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
@ -29,7 +29,7 @@ use webauthn_rs_proto::{
|
|||
RequestAuthenticationExtensions, UserVerificationPolicy,
|
||||
};
|
||||
|
||||
pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
||||
static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
|
||||
let domain = CONFIG.domain();
|
||||
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();
|
||||
|
@ -40,11 +40,9 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
|||
.rp_name(&domain)
|
||||
.timeout(Duration::from_millis(60000));
|
||||
|
||||
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> {
|
||||
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
|
||||
}
|
||||
|
@ -130,12 +128,7 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
|
|||
}
|
||||
|
||||
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
||||
async fn generate_webauthn_challenge(
|
||||
data: Json<PasswordOrOtpData>,
|
||||
headers: Headers,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let data: PasswordOrOtpData = data.into_inner();
|
||||
let user = headers.user;
|
||||
|
||||
|
@ -148,7 +141,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
|
||||
.collect();
|
||||
|
||||
let (mut challenge, state) = webauthn.start_passkey_registration(
|
||||
let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
|
||||
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
||||
&user.email,
|
||||
&user.name,
|
||||
|
@ -259,12 +252,7 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
|
|||
}
|
||||
|
||||
#[post("/two-factor/webauthn", data = "<data>")]
|
||||
async fn activate_webauthn(
|
||||
data: Json<EnableWebauthnData>,
|
||||
headers: Headers,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let data: EnableWebauthnData = data.into_inner();
|
||||
let mut user = headers.user;
|
||||
|
||||
|
@ -287,7 +275,7 @@ async fn activate_webauthn(
|
|||
};
|
||||
|
||||
// Verify the credentials with the saved state
|
||||
let credential = webauthn.finish_passkey_registration(&data.device_response.into(), &state)?;
|
||||
let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?;
|
||||
|
||||
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
||||
// TODO: Check for repeated ID's
|
||||
|
@ -316,13 +304,8 @@ async fn activate_webauthn(
|
|||
}
|
||||
|
||||
#[put("/two-factor/webauthn", data = "<data>")]
|
||||
async fn activate_webauthn_put(
|
||||
data: Json<EnableWebauthnData>,
|
||||
headers: Headers,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
conn: DbConn,
|
||||
) -> JsonResult {
|
||||
activate_webauthn(data, headers, webauthn, conn).await
|
||||
async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
activate_webauthn(data, headers, conn).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -392,11 +375,7 @@ pub async fn get_webauthn_registrations(
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn generate_webauthn_login(
|
||||
user_id: &UserId,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
conn: &mut DbConn,
|
||||
) -> JsonResult {
|
||||
pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
|
||||
// Load saved credentials
|
||||
let creds: Vec<Passkey> =
|
||||
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(
|
|||
}
|
||||
|
||||
// Generate a challenge based on the credentials
|
||||
let (mut response, state) = webauthn.start_passkey_authentication(&creds)?;
|
||||
let (mut response, state) = WEBAUTHN.start_passkey_authentication(&creds)?;
|
||||
|
||||
// Modify to discourage user verification
|
||||
let mut state = serde_json::to_value(&state)?;
|
||||
|
@ -436,12 +415,7 @@ pub async fn generate_webauthn_login(
|
|||
Ok(Json(serde_json::to_value(response.public_key)?))
|
||||
}
|
||||
|
||||
pub async fn validate_webauthn_login(
|
||||
user_id: &UserId,
|
||||
response: &str,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
|
||||
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
||||
let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
||||
Some(tf) => {
|
||||
|
@ -467,7 +441,7 @@ pub async fn validate_webauthn_login(
|
|||
// Because of this we check the flag at runtime and update the registrations and state when needed
|
||||
check_and_update_backup_eligible(user_id, &rsp, &mut registrations, &mut state, conn).await?;
|
||||
|
||||
let authentication_result = webauthn.finish_passkey_authentication(&rsp, &state)?;
|
||||
let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?;
|
||||
|
||||
for reg in &mut registrations {
|
||||
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {
|
||||
|
|
|
@ -9,7 +9,6 @@ use rocket::{
|
|||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::api::core::two_factor::webauthn::Webauthn2FaConfig;
|
||||
use crate::{
|
||||
api::{
|
||||
core::{
|
||||
|
@ -49,7 +48,6 @@ async fn login(
|
|||
data: Form<ConnectData>,
|
||||
client_header: ClientHeaders,
|
||||
client_version: Option<ClientVersion>,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
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_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" => {
|
||||
_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_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"),
|
||||
t => err!("Invalid type", t),
|
||||
|
@ -171,7 +169,6 @@ async fn _sso_login(
|
|||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
client_version: &Option<ClientVersion>,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
) -> JsonResult {
|
||||
AuthMethod::Sso.check_scope(data.scope.as_ref())?;
|
||||
|
||||
|
@ -270,7 +267,7 @@ async fn _sso_login(
|
|||
}
|
||||
Some((mut user, sso_user)) => {
|
||||
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() {
|
||||
// User was invited a stub was created
|
||||
|
@ -325,7 +322,6 @@ async fn _password_login(
|
|||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
client_version: &Option<ClientVersion>,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
) -> JsonResult {
|
||||
// Validate scope
|
||||
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 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);
|
||||
|
||||
|
@ -667,7 +663,6 @@ async fn twofactor_auth(
|
|||
device: &mut Device,
|
||||
ip: &ClientIp,
|
||||
client_version: &Option<ClientVersion>,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
conn: &mut DbConn,
|
||||
) -> ApiResult<Option<String>> {
|
||||
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
||||
|
@ -687,7 +682,7 @@ async fn twofactor_auth(
|
|||
Some(ref code) => code,
|
||||
None => {
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
@ -704,9 +699,7 @@ async fn twofactor_auth(
|
|||
Some(TwoFactorType::Authenticator) => {
|
||||
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
|
||||
}
|
||||
Some(TwoFactorType::Webauthn) => {
|
||||
webauthn::validate_webauthn_login(&user.uuid, twofactor_code, webauthn, conn).await?
|
||||
}
|
||||
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
|
||||
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
||||
Some(TwoFactorType::Duo) => {
|
||||
match CONFIG.duo_use_iframe() {
|
||||
|
@ -738,7 +731,7 @@ async fn twofactor_auth(
|
|||
}
|
||||
_ => {
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
@ -772,7 +765,6 @@ async fn _json_err_twofactor(
|
|||
user_id: &UserId,
|
||||
data: &ConnectData,
|
||||
client_version: &Option<ClientVersion>,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
conn: &mut DbConn,
|
||||
) -> ApiResult<Value> {
|
||||
let mut result = json!({
|
||||
|
@ -792,7 +784,7 @@ async fn _json_err_twofactor(
|
|||
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
|
|||
"sso_enabled": CONFIG.sso_enabled(),
|
||||
"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(),
|
||||
"webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(),
|
||||
});
|
||||
|
||||
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
|
||||
|
|
|
@ -1525,6 +1525,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.
|
||||
pub fn is_admin_token_set(&self) -> bool {
|
||||
let token = self.admin_token();
|
||||
|
|
|
@ -61,7 +61,6 @@ mod sso_client;
|
|||
mod util;
|
||||
|
||||
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::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
||||
pub use config::{PathType, CONFIG};
|
||||
|
@ -601,7 +600,6 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
|||
.manage(pool)
|
||||
.manage(Arc::clone(&WS_USERS))
|
||||
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
||||
.manage(Arc::clone(&WEBAUTHN_2FA_CONFIG))
|
||||
.attach(util::AppHeaders())
|
||||
.attach(util::Cors())
|
||||
.attach(util::BetterLogging(extra_debug))
|
||||
|
|
|
@ -172,6 +172,13 @@ app-root a[routerlink="/signup"] {
|
|||
}
|
||||
{{/unless}}
|
||||
|
||||
{{#unless webauthn_2fa_supported}}
|
||||
/* Hide `Passkey` 2FA if it is not supported */
|
||||
.providers-2fa-7 {
|
||||
@extend %vw-hide;
|
||||
}
|
||||
{{/unless}}
|
||||
|
||||
{{#unless emergency_access_allowed}}
|
||||
/* Hide Emergency Access if not allowed */
|
||||
bit-nav-item[route="settings/emergency-access"] {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue