feat(config): Add passkey_login_allowed config option

This commit is contained in:
Raphaël Roumezin 2026-06-24 11:44:53 +02:00
parent 007ac4f992
commit 8cba57a1af
6 changed files with 55 additions and 4 deletions

View File

@ -316,6 +316,9 @@
## unauthenticated access to potentially sensitive data.
# SHOW_PASSWORD_HINT=false
## Controls whether users can setup and use passkeys to log in, which also bypasses 2FA. This setting applies globally to all users.
# PASSKEY_LOGIN_ALLOWED=true
#########################
### Advanced settings ###
#########################

View File

@ -4,6 +4,7 @@ use webauthn_rs::prelude::{Passkey, PasskeyRegistration};
use webauthn_rs_proto::{COSEAlgorithm, UserVerificationPolicy, options::PubKeyCredParams};
use crate::{
CONFIG,
api::{
ApiResult, JsonResult, PasswordOrOtpData,
core::two_factor::webauthn::{RegisterPublicKeyCredentialCopy, WEBAUTHN},
@ -20,7 +21,16 @@ pub fn routes() -> Vec<rocket::Route> {
}
#[get("/webauthn")]
async fn get_webauthn(headers: Headers, conn: DbConn) -> Json<Value> {
async fn get_webauthn(headers: Headers, conn: DbConn) -> JsonResult {
if !CONFIG.passkey_login_allowed() {
// The web vault will query this endpoint weather passkey login is allowed, so we should return an empty list instead of an error.
return Ok(Json(json!({
"object": "list",
"data": [],
"continuationToken": null
})))
}
let user = headers.user;
let data: Vec<WebauthnCredential> = WebauthnCredential::find_all_by_user(&user.uuid, &conn).await;
@ -38,11 +48,11 @@ async fn get_webauthn(headers: Headers, conn: DbConn) -> Json<Value> {
})
.collect::<Value>();
Json(json!({
Ok(Json(json!({
"object": "list",
"data": data,
"continuationToken": null
}))
})))
}
#[post("/webauthn/attestation-options", data = "<data>")]
@ -51,6 +61,10 @@ async fn post_webauthn_attestation_options(
headers: Headers,
conn: DbConn,
) -> JsonResult {
if !CONFIG.passkey_login_allowed() {
err!("Passkey login is not allowed")
}
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
@ -165,6 +179,10 @@ async fn post_webauthn(
headers: Headers,
conn: DbConn,
) -> ApiResult<Status> {
if !CONFIG.passkey_login_allowed() {
err!("Passkey login is not allowed")
}
let data: WebAuthnLoginCredentialCreateRequest = data.into_inner();
let user = headers.user;
@ -201,6 +219,10 @@ async fn post_webauthn_delete(
headers: Headers,
conn: DbConn,
) -> ApiResult<Status> {
if !CONFIG.passkey_login_allowed() {
err!("Passkey login is not allowed")
}
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;

View File

@ -114,7 +114,7 @@ async fn login(
sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
}
"authorization_code" => err!("SSO sign-in is not available"),
"webauthn" => {
"webauthn" if CONFIG.passkey_login_allowed() => {
check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
check_is_some(data.scope.as_ref(), "scope cannot be blank")?;
@ -127,6 +127,7 @@ async fn login(
webauthn_login(data, &mut user_id, &conn, &client_header.ip).await
}
"webauthn" => err!("Passkey login is not allowed"),
t => err!("Invalid type", t),
};
@ -1490,6 +1491,10 @@ async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure,
#[get("/accounts/webauthn/assertion-options")]
fn get_webauthn_assertion_options() -> JsonResult {
if !CONFIG.passkey_login_allowed() {
err!("Passkey login is not allowed")
}
let (mut response, state) = WEBAUTHN.start_passkey_authentication(&[])?;
// Allow any credential (discoverable) and require user verification

View File

@ -78,6 +78,7 @@ fn vaultwarden_css() -> Cached<Css<String>> {
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_only(),
"webauthn_2fa_supported": CONFIG.is_webauthn_2fa_supported(),
"yubico_enabled": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),
"passkey_login_allowed": CONFIG.passkey_login_allowed(),
});
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {

View File

@ -649,6 +649,9 @@ make_config! {
/// because this provides unauthenticated access to potentially sensitive data.
show_password_hint: bool, true, def, false;
/// Allow passkey login |> Controls whether users can setup and use passkeys to log in, which also bypasses 2FA. This setting applies globally to all users.
passkey_login_allowed: bool, true, def, true;
/// Admin token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session!
admin_token: Pass, true, option;

View File

@ -54,6 +54,23 @@ app-root ng-component > form > div:nth-child(1) > div > button[buttontype="secon
}
{{/if}}
{{#if (not passkey_login_allowed)}}
/* Hide the `Log in with passkey` settings */
app-user-layout app-password-settings app-webauthn-login-settings {
@extend %vw-hide;
}
/* Hide Log in with passkey on the login page */
{{#if (webver ">=2025.5.1")}}
.vw-passkey-login {
@extend %vw-hide;
}
{{else}}
app-root ng-component > form > div:nth-child(1) > div > button[buttontype="secondary"].\!tw-text-primary-600:nth-child(3) {
@extend %vw-hide;
}
{{/if}}
{{/if}}
/* Hide the or text followed by the two buttons hidden above */
{{#if (webver ">=2025.5.1")}}
{{#if (or (not sso_enabled) sso_only)}}