mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-09 19:22:42 +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 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, UserVerificationPolicy,
|
RequestAuthenticationExtensions, UserVerificationPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
||||||
|
@ -40,11 +40,9 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
||||||
.rp_name(&domain)
|
.rp_name(&domain)
|
||||||
.timeout(Duration::from_millis(60000));
|
.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> {
|
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,]
|
||||||
}
|
}
|
||||||
|
@ -130,12 +128,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;
|
||||||
|
|
||||||
|
@ -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
|
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
|
||||||
.collect();
|
.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
|
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
||||||
&user.email,
|
&user.email,
|
||||||
&user.name,
|
&user.name,
|
||||||
|
@ -259,12 +252,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;
|
||||||
|
|
||||||
|
@ -287,7 +275,7 @@ async fn activate_webauthn(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify the credentials with the saved state
|
// 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;
|
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
|
||||||
|
@ -316,13 +304,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)]
|
||||||
|
@ -392,11 +375,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<Passkey> =
|
let creds: Vec<Passkey> =
|
||||||
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
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
|
// 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
|
// Modify to discourage user verification
|
||||||
let mut state = serde_json::to_value(&state)?;
|
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)?))
|
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 mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
||||||
Some(tf) => {
|
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
|
// 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?;
|
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 {
|
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()) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
@ -667,7 +663,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;
|
||||||
|
@ -687,7 +682,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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -704,9 +699,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() {
|
||||||
|
@ -738,7 +731,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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -772,7 +765,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!({
|
||||||
|
@ -792,7 +784,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
/// 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();
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -172,6 +172,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…
Add table
Add a link
Reference in a new issue