rwjack 1 week ago
committed by GitHub
parent
commit
e41d1f701d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .env.template
  2. 4
      migrations/mysql/2026-03-29-120000_add_device_trusted_encryption/down.sql
  3. 4
      migrations/mysql/2026-03-29-120000_add_device_trusted_encryption/up.sql
  4. 4
      migrations/postgresql/2026-03-29-120000_add_device_trusted_encryption/down.sql
  5. 4
      migrations/postgresql/2026-03-29-120000_add_device_trusted_encryption/up.sql
  6. 0
      migrations/sqlite/2026-03-29-120000_add_device_trusted_encryption/down.sql
  7. 3
      migrations/sqlite/2026-03-29-120000_add_device_trusted_encryption/up.sql
  8. 1
      playwright/docker-compose.yml
  9. 26
      playwright/tests/sso_trusted_device.spec.ts
  10. 49
      src/api/core/accounts.rs
  11. 25
      src/api/core/ciphers.rs
  12. 3
      src/api/core/sends.rs
  13. 57
      src/api/identity.rs
  14. 1
      src/api/mod.rs
  15. 218
      src/api/user_decryption.rs
  16. 2
      src/config.rs
  17. 35
      src/db/models/device.rs
  18. 9
      src/db/models/user.rs
  19. 3
      src/db/schema.rs
  20. 10
      src/main.rs

2
.env.template

@ -531,6 +531,8 @@
## Log all the tokens, LOG_LEVEL=debug is required
# SSO_DEBUG_TOKENS=false
## Trusted Device Encryption (TDE) for SSO — adds TrustedDeviceOption to SSO login responses (Bitwarden-compatible).
# SSO_TRUSTED_DEVICE_ENCRYPTION=false
########################
### MFA/2FA settings ###

4
migrations/mysql/2026-03-29-120000_add_device_trusted_encryption/down.sql

@ -0,0 +1,4 @@
ALTER TABLE devices
DROP COLUMN encrypted_private_key,
DROP COLUMN encrypted_public_key,
DROP COLUMN encrypted_user_key;

4
migrations/mysql/2026-03-29-120000_add_device_trusted_encryption/up.sql

@ -0,0 +1,4 @@
ALTER TABLE devices
ADD COLUMN encrypted_private_key TEXT NULL,
ADD COLUMN encrypted_public_key TEXT NULL,
ADD COLUMN encrypted_user_key TEXT NULL;

4
migrations/postgresql/2026-03-29-120000_add_device_trusted_encryption/down.sql

@ -0,0 +1,4 @@
ALTER TABLE devices
DROP COLUMN IF EXISTS encrypted_private_key,
DROP COLUMN IF EXISTS encrypted_public_key,
DROP COLUMN IF EXISTS encrypted_user_key;

4
migrations/postgresql/2026-03-29-120000_add_device_trusted_encryption/up.sql

@ -0,0 +1,4 @@
ALTER TABLE devices
ADD COLUMN encrypted_private_key TEXT NULL,
ADD COLUMN encrypted_public_key TEXT NULL,
ADD COLUMN encrypted_user_key TEXT NULL;

0
migrations/sqlite/2026-03-29-120000_add_device_trusted_encryption/down.sql

3
migrations/sqlite/2026-03-29-120000_add_device_trusted_encryption/up.sql

@ -0,0 +1,3 @@
ALTER TABLE devices ADD COLUMN encrypted_private_key TEXT;
ALTER TABLE devices ADD COLUMN encrypted_public_key TEXT;
ALTER TABLE devices ADD COLUMN encrypted_user_key TEXT;

1
playwright/docker-compose.yml

@ -35,6 +35,7 @@ services:
- SSO_FRONTEND
- SSO_ONLY
- SSO_SCOPES
- SSO_TRUSTED_DEVICE_ENCRYPTION
restart: "no"
depends_on:
- VaultwardenPrebuild

26
playwright/tests/sso_trusted_device.spec.ts

@ -0,0 +1,26 @@
import { test, expect, type TestInfo } from '@playwright/test';
import * as utils from '../global-utils';
/**
* Web-first checks for SSO + trusted-device (TDE) support:
* - `sso-connector.html` must be served for browser OIDC redirect.
*/
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVault(browser, testInfo, {
SSO_ENABLED: 'true',
SSO_ONLY: 'false',
SSO_TRUSTED_DEVICE_ENCRYPTION: 'true',
});
});
test.afterAll('Teardown', async () => {
utils.stopVault();
});
test('Web vault serves sso-connector.html for browser SSO', async ({ request }) => {
const res = await request.get('/sso-connector.html');
expect(res.ok(), await res.text()).toBeTruthy();
const ct = res.headers()['content-type'] || '';
expect(ct).toMatch(/text\/html/i);
});

49
src/api/core/accounts.rs

@ -65,6 +65,10 @@ pub fn routes() -> Vec<rocket::Route> {
put_device_token,
put_clear_device_token,
post_clear_device_token,
put_device_keys,
post_device_keys,
put_device_keys_by_uuid,
post_device_keys_by_uuid,
get_tasks,
post_auth_request,
get_auth_request,
@ -1448,6 +1452,51 @@ async fn post_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResu
put_clear_device_token(device_id, conn).await
}
// https://github.com/bitwarden/server/blob/v2026.3.1/src/Api/Controllers/DevicesController.cs
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DeviceKeysData {
encrypted_user_key: String,
encrypted_public_key: String,
encrypted_private_key: String,
}
#[put("/devices/identifier/<device_id>/keys", data = "<data>")]
async fn put_device_keys(device_id: DeviceId, data: Json<DeviceKeysData>, headers: Headers, conn: DbConn) -> JsonResult {
if headers.device.uuid != device_id {
err!("No device found");
}
let Some(mut device) = Device::find_by_uuid_and_user(&device_id, &headers.user.uuid, &conn).await else {
err!("No device found");
};
let data = data.into_inner();
if data.encrypted_user_key.is_empty() || data.encrypted_public_key.is_empty() || data.encrypted_private_key.is_empty()
{
err!("Invalid device keys");
}
device.encrypted_user_key = Some(data.encrypted_user_key);
device.encrypted_public_key = Some(data.encrypted_public_key);
device.encrypted_private_key = Some(data.encrypted_private_key);
device.save(true, &conn).await?;
Ok(Json(device.to_json()))
}
#[post("/devices/identifier/<device_id>/keys", data = "<data>")]
async fn post_device_keys(device_id: DeviceId, data: Json<DeviceKeysData>, headers: Headers, conn: DbConn) -> JsonResult {
put_device_keys(device_id, data, headers, conn).await
}
// Bitwarden server: `PUT|POST devices/{identifier}/keys` (not `devices/identifier/.../keys`).
#[put("/devices/<device_id>/keys", data = "<data>")]
async fn put_device_keys_by_uuid(device_id: DeviceId, data: Json<DeviceKeysData>, headers: Headers, conn: DbConn) -> JsonResult {
put_device_keys(device_id, data, headers, conn).await
}
#[post("/devices/<device_id>/keys", data = "<data>")]
async fn post_device_keys_by_uuid(device_id: DeviceId, data: Json<DeviceKeysData>, headers: Headers, conn: DbConn) -> JsonResult {
put_device_keys(device_id, data, headers, conn).await
}
#[get("/tasks")]
fn get_tasks(_client_headers: ClientHeaders) -> JsonResult {
Ok(Json(json!({

25
src/api/core/ciphers.rs

@ -163,26 +163,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
api::core::_get_eq_domains(&headers, true).into_inner()
};
// This is very similar to the the userDecryptionOptions sent in connect/token,
// but as of 2025-12-19 they're both using different casing conventions.
let has_master_password = !headers.user.password_hash.is_empty();
let master_password_unlock = if has_master_password {
json!({
"kdf": {
"kdfType": headers.user.client_kdf_type,
"iterations": headers.user.client_kdf_iter,
"memory": headers.user.client_kdf_memory,
"parallelism": headers.user.client_kdf_parallelism
},
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
"masterKeyEncryptedUserKey": headers.user.akey,
"masterKeyWrappedUserKey": headers.user.akey,
"salt": headers.user.email
})
} else {
Value::Null
};
let user_decryption = api::user_decryption::build_sync_user_decryption(&headers.user, &headers.device, &conn).await;
Ok(Json(json!({
"profile": user_json,
@ -192,9 +173,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
"ciphers": ciphers_json,
"domains": domains_json,
"sends": sends_json,
"userDecryption": {
"masterPasswordUnlock": master_password_unlock,
},
"userDecryption": user_decryption,
"object": "sync"
})))
}

3
src/api/core/sends.rs

@ -35,6 +35,9 @@ static ANON_PUSH_DEVICE: LazyLock<Device> = LazyLock::new(|| {
push_token: None,
refresh_token: String::new(),
twofactor_remember: None,
encrypted_private_key: None,
encrypted_public_key: None,
encrypted_user_key: None,
}
});

57
src/api/identity.rs

@ -304,7 +304,7 @@ async fn _sso_login(
// We passed 2FA get auth tokens
let auth_tokens = sso::redeem(&device, &user, data.client_id, sso_user, sso_auth, user_infos, conn).await?;
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip, true).await
}
async fn _password_login(
@ -426,7 +426,7 @@ async fn _password_login(
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip, false).await
}
async fn authenticated_response(
@ -436,6 +436,7 @@ async fn authenticated_response(
twofactor_token: Option<String>,
conn: &DbConn,
ip: &ClientIp,
sso_login: bool,
) -> JsonResult {
if CONFIG.mail_enabled() && device.is_new() {
let now = Utc::now().naive_utc();
@ -463,24 +464,8 @@ async fn authenticated_response(
let master_password_policy = master_password_policy(user, conn).await;
let has_master_password = !user.password_hash.is_empty();
let master_password_unlock = if has_master_password {
json!({
"Kdf": {
"KdfType": user.client_kdf_type,
"Iterations": user.client_kdf_iter,
"Memory": user.client_kdf_memory,
"Parallelism": user.client_kdf_parallelism
},
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
"MasterKeyEncryptedUserKey": user.akey,
"MasterKeyWrappedUserKey": user.akey,
"Salt": user.email
})
} else {
Value::Null
};
let user_decryption_options =
super::user_decryption::build_token_user_decryption_options(user, device, conn, sso_login).await;
let account_keys = if user.private_key.is_some() {
json!({
@ -510,11 +495,7 @@ async fn authenticated_response(
"MasterPasswordPolicy": master_password_policy,
"scope": auth_tokens.scope(),
"AccountKeys": account_keys,
"UserDecryptionOptions": {
"HasMasterPassword": has_master_password,
"MasterPasswordUnlock": master_password_unlock,
"Object": "userDecryptionOptions"
},
"UserDecryptionOptions": user_decryption_options,
});
if !user.akey.is_empty() {
@ -614,24 +595,8 @@ async fn _user_api_key_login(
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
let has_master_password = !user.password_hash.is_empty();
let master_password_unlock = if has_master_password {
json!({
"Kdf": {
"KdfType": user.client_kdf_type,
"Iterations": user.client_kdf_iter,
"Memory": user.client_kdf_memory,
"Parallelism": user.client_kdf_parallelism
},
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
"MasterKeyEncryptedUserKey": user.akey,
"MasterKeyWrappedUserKey": user.akey,
"Salt": user.email
})
} else {
Value::Null
};
let user_decryption_options =
super::user_decryption::build_token_user_decryption_options(&user, &device, conn, false).await;
let account_keys = if user.private_key.is_some() {
json!({
@ -663,11 +628,7 @@ async fn _user_api_key_login(
"ForcePasswordReset": false,
"scope": AuthMethod::UserApiKey.scope(),
"AccountKeys": account_keys,
"UserDecryptionOptions": {
"HasMasterPassword": has_master_password,
"MasterPasswordUnlock": master_password_unlock,
"Object": "userDecryptionOptions"
},
"UserDecryptionOptions": user_decryption_options,
});
Ok(Json(result))

1
src/api/mod.rs

@ -4,6 +4,7 @@ mod icons;
mod identity;
mod notifications;
mod push;
pub(crate) mod user_decryption;
mod web;
use rocket::serde::json::Json;

218
src/api/user_decryption.rs

@ -0,0 +1,218 @@
//! `UserDecryptionOptions` (login) and `userDecryption` (sync) payloads for Bitwarden-compatible clients.
//!
//! References: Bitwarden `UserDecryptionOptionsBuilder`, `TrustedDeviceUserDecryptionOption`, and
//! `libs/common/.../user-decryption-options.response.ts` in bitwarden/clients.
use serde_json::{json, Value};
use crate::db::models::{Device, Membership, SsoUser, User, UserId};
use crate::db::DbConn;
use crate::CONFIG;
/// Device types that may approve “login with device” / trusted-device flows (see Bitwarden `LoginApprovingClientTypes`).
pub fn device_type_can_approve_trusted_login(atype: i32) -> bool {
!matches!(atype, 21..=25) // SDK, Server, CLIs
}
async fn has_login_approving_device(user_uuid: &UserId, current: &Device, conn: &DbConn) -> bool {
let devices = Device::find_by_user(user_uuid, conn).await;
devices.iter().any(|d| {
d.uuid != current.uuid
&& device_type_can_approve_trusted_login(d.atype)
})
}
fn has_valid_reset_password_key(m: &Membership) -> bool {
m.reset_password_key.as_ref().is_some_and(|s| !s.trim().is_empty())
}
/// Owner or Admin (Vaultwarden does not persist custom-role JSON for `manageResetPassword` on members).
fn membership_has_manage_reset_password(m: &Membership) -> bool {
matches!(m.atype, 0 | 1)
}
async fn aggregate_trusted_device_flags(user: &User, device: &Device, conn: &DbConn) -> (bool, bool, bool) {
let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let has_admin_approval = members.iter().any(has_valid_reset_password_key);
let has_manage_reset = members.iter().any(membership_has_manage_reset_password);
let has_login_approving = has_login_approving_device(&user.uuid, device, conn).await;
(has_admin_approval, has_manage_reset, has_login_approving)
}
/// Sync may be called long after SSO login; include TDE hints for users linked to SSO.
async fn user_in_sso_context(user_uuid: &UserId, conn: &DbConn) -> bool {
if !CONFIG.sso_enabled() {
return false;
}
SsoUser::find_by_user(user_uuid, conn).await.is_some()
}
fn trusted_device_option_token(
has_admin_approval: bool,
has_login_approving_device: bool,
has_manage_reset_password_permission: bool,
is_tde_offboarding: bool,
device: &Device,
) -> Value {
let (enc_priv, enc_user) = if device.is_trusted() {
(
device
.encrypted_private_key
.as_ref()
.filter(|s| !s.is_empty())
.map(|s| json!(s))
.unwrap_or(Value::Null),
device
.encrypted_user_key
.as_ref()
.filter(|s| !s.is_empty())
.map(|s| json!(s))
.unwrap_or(Value::Null),
)
} else {
(Value::Null, Value::Null)
};
json!({
"HasAdminApproval": has_admin_approval,
"HasLoginApprovingDevice": has_login_approving_device,
"HasManageResetPasswordPermission": has_manage_reset_password_permission,
"IsTdeOffboarding": is_tde_offboarding,
"EncryptedPrivateKey": enc_priv,
"EncryptedUserKey": enc_user,
})
}
fn trusted_device_option_sync(
has_admin_approval: bool,
has_login_approving_device: bool,
has_manage_reset_password_permission: bool,
is_tde_offboarding: bool,
device: &Device,
) -> Value {
let (enc_priv, enc_user) = if device.is_trusted() {
(
device
.encrypted_private_key
.as_ref()
.filter(|s| !s.is_empty())
.map(|s| json!(s))
.unwrap_or(Value::Null),
device
.encrypted_user_key
.as_ref()
.filter(|s| !s.is_empty())
.map(|s| json!(s))
.unwrap_or(Value::Null),
)
} else {
(Value::Null, Value::Null)
};
json!({
"hasAdminApproval": has_admin_approval,
"hasLoginApprovingDevice": has_login_approving_device,
"hasManageResetPasswordPermission": has_manage_reset_password_permission,
"isTdeOffboarding": is_tde_offboarding,
"encryptedPrivateKey": enc_priv,
"encryptedUserKey": enc_user,
})
}
/// `UserDecryptionOptions` for `POST /identity/connect/token` (PascalCase, Bitwarden Identity).
pub async fn build_token_user_decryption_options(
user: &User,
device: &Device,
conn: &DbConn,
sso_login: bool,
) -> Value {
let has_master_password = !user.password_hash.is_empty();
let master_password_unlock = if has_master_password {
json!({
"Kdf": {
"KdfType": user.client_kdf_type,
"Iterations": user.client_kdf_iter,
"Memory": user.client_kdf_memory,
"Parallelism": user.client_kdf_parallelism
},
"MasterKeyEncryptedUserKey": user.akey,
"MasterKeyWrappedUserKey": user.akey,
"Salt": user.email
})
} else {
Value::Null
};
let mut out = json!({
"HasMasterPassword": has_master_password,
"MasterPasswordUnlock": master_password_unlock,
"Object": "userDecryptionOptions"
});
// Bitwarden only builds trusted-device options when SSO Identity context exists (authorization_code grant).
if !sso_login {
return out;
}
let is_tde_active = CONFIG.sso_trusted_device_encryption();
let is_tde_offboarding = !has_master_password && device.is_trusted() && !is_tde_active;
if !is_tde_active && !is_tde_offboarding {
return out;
}
let (ha, hm, hl) = aggregate_trusted_device_flags(user, device, conn).await;
out["TrustedDeviceOption"] = trusted_device_option_token(ha, hl, hm, is_tde_offboarding, device);
out
}
/// `userDecryption` object on full sync (camelCase nested keys; see `GET /sync`).
pub async fn build_sync_user_decryption(user: &User, device: &Device, conn: &DbConn) -> Value {
let has_master_password = !user.password_hash.is_empty();
let master_password_unlock = if has_master_password {
json!({
"kdf": {
"kdfType": user.client_kdf_type,
"iterations": user.client_kdf_iter,
"memory": user.client_kdf_memory,
"parallelism": user.client_kdf_parallelism
},
"masterKeyEncryptedUserKey": user.akey,
"masterKeyWrappedUserKey": user.akey,
"salt": user.email
})
} else {
Value::Null
};
let mut out = json!({
"masterPasswordUnlock": master_password_unlock,
});
if !user_in_sso_context(&user.uuid, conn).await {
return out;
}
let is_tde_active = CONFIG.sso_trusted_device_encryption();
let is_tde_offboarding = !has_master_password && device.is_trusted() && !is_tde_active;
if !is_tde_active && !is_tde_offboarding {
return out;
}
let (ha, hm, hl) = aggregate_trusted_device_flags(user, device, conn).await;
out["trustedDeviceOption"] = trusted_device_option_sync(ha, hl, hm, is_tde_offboarding, device);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_type_approver_excludes_cli_and_server() {
assert!(device_type_can_approve_trusted_login(14));
assert!(!device_type_can_approve_trusted_login(22));
assert!(!device_type_can_approve_trusted_login(23));
}
}

2
src/config.rs

@ -832,6 +832,8 @@ make_config! {
sso_client_cache_expiration: u64, true, def, 0;
/// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required
sso_debug_tokens: bool, true, def, false;
/// Trusted Device Encryption (TDE) for SSO |> When enabled, SSO token responses include `TrustedDeviceOption` per Bitwarden Identity (`UserDecryptionOptions`). Requires clients that support TDE. See: https://bitwarden.com/help/sso-decryption-options/
sso_trusted_device_encryption: bool, true, def, false;
},
/// Yubikey settings

35
src/db/models/device.rs

@ -31,6 +31,13 @@ pub struct Device {
pub refresh_token: String,
pub twofactor_remember: Option<String>,
/// Device private key encrypted with the device key (trusted-device / TDE).
pub encrypted_private_key: Option<String>,
/// Device public key encrypted with the user key.
pub encrypted_public_key: Option<String>,
/// User symmetric key encrypted with the device public key.
pub encrypted_user_key: Option<String>,
}
/// Local methods
@ -51,6 +58,10 @@ impl Device {
push_token: None,
refresh_token: Device::generate_refresh_token(),
twofactor_remember: None,
encrypted_private_key: None,
encrypted_public_key: None,
encrypted_user_key: None,
}
}
@ -59,6 +70,13 @@ impl Device {
crypto::encode_random_bytes::<64>(&BASE64URL)
}
/// Matches upstream `DeviceExtensions.IsTrusted` / device list responses.
pub fn is_trusted(&self) -> bool {
self.encrypted_user_key.as_ref().is_some_and(|s| !s.is_empty())
&& self.encrypted_public_key.as_ref().is_some_and(|s| !s.is_empty())
&& self.encrypted_private_key.as_ref().is_some_and(|s| !s.is_empty())
}
pub fn to_json(&self) -> Value {
json!({
"id": self.uuid,
@ -66,11 +84,20 @@ impl Device {
"type": self.atype,
"identifier": self.uuid,
"creationDate": format_date(&self.created_at),
"isTrusted": false,
"isTrusted": self.is_trusted(),
"encryptedUserKey": Self::enc_string_json(&self.encrypted_user_key),
"encryptedPublicKey": Self::enc_string_json(&self.encrypted_public_key),
"object":"device"
})
}
fn enc_string_json(v: &Option<String>) -> Value {
match v {
Some(s) if !s.is_empty() => Value::String(s.clone()),
_ => Value::Null,
}
}
pub fn refresh_twofactor_remember(&mut self) -> String {
use crate::auth::{encode_jwt, generate_2fa_remember_claims};
@ -121,9 +148,9 @@ impl DeviceWithAuthRequest {
"identifier": self.device.uuid,
"creationDate": format_date(&self.device.created_at),
"devicePendingAuthRequest": auth_request,
"isTrusted": false,
"encryptedPublicKey": null,
"encryptedUserKey": null,
"isTrusted": self.device.is_trusted(),
"encryptedPublicKey": Device::enc_string_json(&self.device.encrypted_public_key),
"encryptedUserKey": Device::enc_string_json(&self.device.encrypted_user_key),
"object": "device",
})
}

9
src/db/models/user.rs

@ -561,4 +561,13 @@ impl SsoUser {
.map_res("Error deleting sso user")
}}
}
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
sso_users::table
.filter(sso_users::user_uuid.eq(user_uuid))
.first::<Self>(conn)
.ok()
}}
}
}

3
src/db/schema.rs

@ -55,6 +55,9 @@ table! {
push_token -> Nullable<Text>,
refresh_token -> Text,
twofactor_remember -> Nullable<Text>,
encrypted_private_key -> Nullable<Text>,
encrypted_public_key -> Nullable<Text>,
encrypted_user_key -> Nullable<Text>,
}
}

10
src/main.rs

@ -542,6 +542,16 @@ fn check_web_vault() {
error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it");
exit(1);
}
if CONFIG.sso_enabled() {
let sso_connector = Path::new(&CONFIG.web_vault_folder()).join("sso-connector.html");
if !sso_connector.is_file() {
warn!(
"Web vault is missing 'sso-connector.html' at '{}'. Browser OIDC SSO redirects to this file; install a current web vault or disable SSO.",
sso_connector.display()
);
}
}
}
async fn create_db_pool() -> db::DbPool {

Loading…
Cancel
Save