From 8333a492110e1fdc21ff7f2a8f18cd73cafef783 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 10:55:29 +0200 Subject: [PATCH 01/22] migrate/generate and persist rsa openid signing key --- .../defguard_common/src/db/models/settings.rs | 141 +++++++++++++++++- ...00_[2.0.0]_openid_signing_key_der.down.sql | 3 + ...3000_[2.0.0]_openid_signing_key_der.up.sql | 3 + 3 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 migrations/20260422153000_[2.0.0]_openid_signing_key_der.down.sql create mode 100644 migrations/20260422153000_[2.0.0]_openid_signing_key_der.up.sql diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 5efb492c0..b6a1d63bd 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -2,6 +2,10 @@ use std::{collections::HashMap, fmt, net::IpAddr, time::Duration}; use base64::{Engine, prelude::BASE64_STANDARD}; use rand::{RngCore, rngs::OsRng}; +use rsa::{ + RsaPrivateKey, + pkcs8::{DecodePrivateKey, EncodePrivateKey}, +}; use secrecy::ExposeSecret; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, Type, query, query_as}; @@ -215,6 +219,7 @@ pub struct Settings { pub default_admin_id: Option, // 1.6 config options pub secret_key: Option, + pub openid_signing_key_der: Option>, pub enable_stats_purge: bool, stats_purge_frequency_hours: i32, stats_purge_threshold_days: i32, @@ -312,6 +317,11 @@ impl fmt::Debug for Settings { .field("mfa_code_timeout_seconds", &self.mfa_code_timeout_seconds) .field("public_proxy_url", &self.public_proxy_url) .field("default_admin_id", &self.default_admin_id) + .field("secret_key", &self.secret_key.as_ref().map(|_| "")) + .field( + "openid_signing_key_der", + &self.openid_signing_key_der.as_ref().map(Vec::len), + ) .finish_non_exhaustive() } } @@ -342,6 +352,39 @@ impl Settings { BASE64_STANDARD.encode(bytes) } + fn generate_openid_signing_key_der() -> Vec { + RsaPrivateKey::new(&mut OsRng, 2048) + .expect("failed to generate OpenID signing key") + .to_pkcs8_der() + .expect("failed to serialize OpenID signing key") + .as_bytes() + .to_vec() + } + + fn validate_openid_signing_key_der(key_der: &[u8]) -> Result<(), SettingsInitializationError> { + RsaPrivateKey::from_pkcs8_der(key_der) + .map(|_| ()) + .map_err(|_| { + SettingsInitializationError::Invalid( + "openid_signing_key_der", + "invalid RSA private key", + ) + }) + } + + fn openid_signing_key_der_from_config( + key: &RsaPrivateKey, + ) -> Result, SettingsInitializationError> { + key.to_pkcs8_der() + .map(|doc| doc.as_bytes().to_vec()) + .map_err(|_| { + SettingsInitializationError::Invalid( + "openid_signing_key_der", + "failed to serialize RSA private key", + ) + }) + } + /// Parse `defguard_url` and reject unsupported host forms. fn parse_defguard_url(&self) -> Result { let url = Url::parse(&self.defguard_url) @@ -426,7 +469,7 @@ impl Settings { defguard_url, \ default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, \ public_proxy_url, \ - default_admin_id, secret_key, enable_stats_purge, \ + default_admin_id, secret_key, openid_signing_key_der, enable_stats_purge, \ stats_purge_frequency_hours, stats_purge_threshold_days, \ enrollment_token_timeout_hours, password_reset_token_timeout_hours, \ enrollment_session_timeout_minutes, password_reset_session_timeout_minutes \ @@ -542,13 +585,14 @@ impl Settings { public_proxy_url = $56, \ default_admin_id = $57, \ secret_key = $58, \ - enable_stats_purge = $59, \ - stats_purge_frequency_hours = $60, \ - stats_purge_threshold_days = $61, \ - enrollment_token_timeout_hours = $62, \ - password_reset_token_timeout_hours = $63, \ - enrollment_session_timeout_minutes = $64, \ - password_reset_session_timeout_minutes = $65 \ + openid_signing_key_der = $59, \ + enable_stats_purge = $60, \ + stats_purge_frequency_hours = $61, \ + stats_purge_threshold_days = $62, \ + enrollment_token_timeout_hours = $63, \ + password_reset_token_timeout_hours = $64, \ + enrollment_session_timeout_minutes = $65, \ + password_reset_session_timeout_minutes = $66 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -608,6 +652,7 @@ impl Settings { self.public_proxy_url, self.default_admin_id, self.secret_key, + &self.openid_signing_key_der as &Option>, self.enable_stats_purge, self.stats_purge_frequency_hours, self.stats_purge_threshold_days, @@ -664,6 +709,16 @@ impl Settings { } } + match settings.openid_signing_key_der.as_deref() { + Some(key_der) => { + Settings::validate_openid_signing_key_der(key_der)?; + } + None => { + settings.openid_signing_key_der = + Some(Settings::generate_openid_signing_key_der()); + } + } + update_current_settings(pool, settings).await?; Ok(()) @@ -811,6 +866,19 @@ impl Settings { self.secret_key = Some(secret_key.to_string()); } } + if let Some(openid_signing_key) = &config.openid_signing_key { + match Settings::openid_signing_key_der_from_config(openid_signing_key) { + Ok(key_der) => { + self.openid_signing_key_der = Some(key_der); + } + Err(err) => { + warn!( + "Invalid openid_signing_key provided in deprecated config, generating new one: {err}" + ); + self.openid_signing_key_der = Some(Settings::generate_openid_signing_key_der()); + } + } + } if let Some(enrollment_url) = &config.enrollment_url { self.public_proxy_url = enrollment_url.to_string(); } @@ -956,6 +1024,7 @@ mod test { use std::str::FromStr; use humantime::Duration; + use rsa::{RsaPrivateKey, pkcs8::EncodePrivateKey}; use reqwest::Url; use secrecy::SecretString; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; @@ -1290,6 +1359,36 @@ mod test { assert_eq!(settings.secret_key.as_deref(), Some(valid_secret.as_str())); } + #[test] + fn test_apply_from_config_valid_openid_signing_key_overwrites_existing_value() { + let mut settings = Settings { + openid_signing_key_der: Some(Settings::generate_openid_signing_key_der()), + ..Default::default() + }; + let mut config = DefGuardConfig::new_test_config(); + let configured_key = RsaPrivateKey::new(&mut OsRng, 2048).unwrap(); + let expected_der = configured_key.to_pkcs8_der().unwrap().as_bytes().to_vec(); + config.openid_signing_key = Some(configured_key); + + settings.apply_from_config(&config); + + assert_eq!(settings.openid_signing_key_der, Some(expected_der)); + } + + #[test] + fn test_apply_from_config_keeps_openid_signing_key_when_config_is_none() { + let existing = Settings::generate_openid_signing_key_der(); + let mut settings = Settings { + openid_signing_key_der: Some(existing.clone()), + ..Default::default() + }; + let config = DefGuardConfig::new_test_config(); + + settings.apply_from_config(&config); + + assert_eq!(settings.openid_signing_key_der, Some(existing)); + } + #[sqlx::test] #[allow(deprecated)] async fn test_update_from_config_persists_and_updates_current_settings( @@ -1348,6 +1447,32 @@ mod test { assert_eq!(from_db.webauthn_rp_id().unwrap(), "defguard.example.com"); } + #[sqlx::test] + async fn test_initialize_runtime_defaults_generates_openid_signing_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + initialize_current_settings(&pool).await.unwrap(); + + Settings::initialize_runtime_defaults(&pool).await.unwrap(); + + let current = Settings::get_current_settings(); + let from_db = Settings::get(&pool).await.unwrap().unwrap(); + + let current_key = current + .openid_signing_key_der + .as_deref() + .expect("current settings should contain OpenID signing key"); + let db_key = from_db + .openid_signing_key_der + .as_deref() + .expect("database settings should contain OpenID signing key"); + + assert!(RsaPrivateKey::from_pkcs8_der(current_key).is_ok()); + assert!(RsaPrivateKey::from_pkcs8_der(db_key).is_ok()); + } + #[test] fn test_edge_callback_url() { let mut s = Settings { diff --git a/migrations/20260422153000_[2.0.0]_openid_signing_key_der.down.sql b/migrations/20260422153000_[2.0.0]_openid_signing_key_der.down.sql new file mode 100644 index 000000000..d8e1d4745 --- /dev/null +++ b/migrations/20260422153000_[2.0.0]_openid_signing_key_der.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE settings + DROP COLUMN IF EXISTS openid_signing_key_der, + ADD COLUMN openid_signing_key TEXT; diff --git a/migrations/20260422153000_[2.0.0]_openid_signing_key_der.up.sql b/migrations/20260422153000_[2.0.0]_openid_signing_key_der.up.sql new file mode 100644 index 000000000..1787447ca --- /dev/null +++ b/migrations/20260422153000_[2.0.0]_openid_signing_key_der.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE settings + DROP COLUMN IF EXISTS openid_signing_key, + ADD COLUMN openid_signing_key_der BYTEA; From 01a11ead02d413668b9d4c7437ad856ae7333d8a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 11:19:10 +0200 Subject: [PATCH 02/22] use the rsa key from db --- crates/defguard/src/main.rs | 2 +- .../defguard_common/src/db/models/settings.rs | 13 +++++++++ .../defguard_core/src/handlers/openid_flow.rs | 6 ++--- .../tests/integration/api/openid.rs | 27 ++++++++++--------- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 78a3272c2..c080e5877 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -94,7 +94,7 @@ async fn main() -> Result<(), anyhow::Error> { ) .await; - if config.openid_signing_key.is_some() { + if Settings::get_current_settings().openid_key().is_some() { info!("Using RSA OpenID signing key"); } else { info!("Using HMAC OpenID signing key"); diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index b6a1d63bd..d23cbc92d 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -2,10 +2,14 @@ use std::{collections::HashMap, fmt, net::IpAddr, time::Duration}; use base64::{Engine, prelude::BASE64_STANDARD}; use rand::{RngCore, rngs::OsRng}; +use openidconnect::{core::CoreRsaPrivateSigningKey, JsonWebKeyId}; use rsa::{ RsaPrivateKey, + pkcs1::EncodeRsaPrivateKey, pkcs8::{DecodePrivateKey, EncodePrivateKey}, + traits::PublicKeyParts, }; +use rsa::pkcs8::LineEnding; use secrecy::ExposeSecret; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, Type, query, query_as}; @@ -385,6 +389,15 @@ impl Settings { }) } + #[must_use] + pub fn openid_key(&self) -> Option { + let key_der = self.openid_signing_key_der.as_deref()?; + let key = RsaPrivateKey::from_pkcs8_der(key_der).ok()?; + let pem = key.to_pkcs1_pem(LineEnding::default()).ok()?; + let key_id = JsonWebKeyId::new(key.n().to_str_radix(36)); + CoreRsaPrivateSigningKey::from_pem(pem.as_ref(), Some(key_id)).ok() + } + /// Parse `defguard_url` and reject unsupported host forms. fn parse_defguard_url(&self) -> Result { let url = Url::parse(&self.defguard_url) diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index 0e926acce..ef7bc17e1 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -94,7 +94,7 @@ impl From<&UserClaims> for StandardClaims { pub async fn discovery_keys() -> ApiResult { let mut keys = Vec::new(); - if let Some(openid_key) = server_config().openid_key() { + if let Some(openid_key) = Settings::get_current_settings().openid_key() { keys.push(openid_key.as_verification_key()); } @@ -874,9 +874,9 @@ pub async fn token( } else { GroupClaims { groups: None } }; - let config = server_config(); let user_claims = UserClaims::from_user(&user, &client, &token); let base_url = Settings::url()?; + let openid_key = Settings::get_current_settings().openid_key(); match form.authorization_code_flow( &auth_code, @@ -884,7 +884,7 @@ pub async fn token( (&user_claims).into(), &base_url, client.client_secret, - config.openid_key(), + openid_key, group_claims, ) { Ok(response) => { diff --git a/crates/defguard_core/tests/integration/api/openid.rs b/crates/defguard_core/tests/integration/api/openid.rs index 3eb607a6f..1a798d535 100644 --- a/crates/defguard_core/tests/integration/api/openid.rs +++ b/crates/defguard_core/tests/integration/api/openid.rs @@ -3,7 +3,10 @@ use std::str::FromStr; use axum::http::header::ToStrError; use defguard_common::db::{ Id, - models::{OAuth2AuthorizedApp, Settings, User, oauth2client::OAuth2Client}, + models::{ + OAuth2AuthorizedApp, Settings, User, oauth2client::OAuth2Client, + settings::update_current_settings, + }, }; use defguard_core::handlers::{Auth, openid_clients::NewOpenIDClient}; use openidconnect::{ @@ -32,6 +35,13 @@ use super::{ }; use crate::api::PaginatedApiResponse; +async fn seed_openid_signing_key(pool: &sqlx::PgPool) { + let mut settings = Settings::get_current_settings(); + let key = RsaPrivateKey::new(&mut rand::thread_rng(), 2048).unwrap(); + settings.openid_signing_key_der = Some(key.to_pkcs8_der().unwrap().as_bytes().to_vec()); + update_current_settings(pool, settings).await.unwrap(); +} + #[derive(Deserialize)] pub struct AuthenticationResponse<'r> { pub code: &'r str, @@ -685,10 +695,7 @@ async fn dg25_25_openid_disabled_client_userinfo_fails( let pool = setup_pool(options).await; let (client, state) = make_test_client(pool).await; - let mut config = state.config; - - let mut rng = rand::thread_rng(); - config.openid_signing_key = RsaPrivateKey::new(&mut rng, 2048).ok(); + seed_openid_signing_key(&state.pool).await; let issuer_url = IssuerUrl::from_url(Settings::url().unwrap().clone()); @@ -816,10 +823,7 @@ async fn test_openid_authorization_code_with_pkce(_: PgPoolOptions, options: PgC let pool = setup_pool(options).await; let (client, state) = make_test_client(pool).await; - let mut config = state.config; - - let mut rng = rand::thread_rng(); - config.openid_signing_key = RsaPrivateKey::new(&mut rng, 2048).ok(); + seed_openid_signing_key(&state.pool).await; let issuer_url = IssuerUrl::from_url(Settings::url().unwrap().clone()); @@ -1265,7 +1269,7 @@ async fn dg25_22_test_respect_openid_scope_in_userinfo( let pool = setup_pool(options).await; let (client, state) = make_test_client(pool).await; - let mut config = state.config; + seed_openid_signing_key(&state.pool).await; let mut admin = User::find_by_username(&state.pool, "admin") .await @@ -1275,9 +1279,6 @@ async fn dg25_22_test_respect_openid_scope_in_userinfo( admin.phone = Some("+123456789".into()); admin.save(&state.pool).await.unwrap(); - let mut rng = rand::thread_rng(); - config.openid_signing_key = RsaPrivateKey::new(&mut rng, 2048).ok(); - let issuer_url = IssuerUrl::from_url(Settings::url().unwrap().clone()); // discover OpenID service From 08bdd3b1f4639f3ae1fb02b6bfa9e4973914e493 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 11:37:45 +0200 Subject: [PATCH 03/22] add deprecated hmac flag for compatibility --- crates/defguard/src/main.rs | 5 +++- crates/defguard_common/src/config.rs | 30 +++++++++++-------- .../defguard_core/src/handlers/openid_flow.rs | 13 ++++++-- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index c080e5877..d6ddd176b 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -94,7 +94,10 @@ async fn main() -> Result<(), anyhow::Error> { ) .await; - if Settings::get_current_settings().openid_key().is_some() { + #[allow(deprecated)] + if config.hmac.unwrap_or(false) { + info!("Using HMAC OpenID signing key (forced by deprecated config flag)"); + } else if Settings::get_current_settings().openid_key().is_some() { info!("Using RSA OpenID signing key"); } else { info!("Using HMAC OpenID signing key"); diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index ef1a96234..52bac0213 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -3,18 +3,18 @@ use std::{net::IpAddr, sync::OnceLock}; use clap::{Args, Parser, Subcommand}; use humantime::Duration; use ipnetwork::IpNetwork; -use openidconnect::{JsonWebKeyId, core::CoreRsaPrivateSigningKey}; +use openidconnect::{core::CoreRsaPrivateSigningKey, JsonWebKeyId}; use reqwest::Url; use rsa::{ - RsaPrivateKey, pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey}, pkcs8::{DecodePrivateKey, LineEnding}, traits::PublicKeyParts, + RsaPrivateKey, }; use secrecy::{ExposeSecret, SecretString}; use serde::Serialize; -use crate::{VERSION, db::models::Settings}; +use crate::{db::models::Settings, VERSION}; pub static SERVER_CONFIG: OnceLock = OnceLock::new(); @@ -74,6 +74,13 @@ pub struct DefGuardConfig { #[serde(skip_serializing)] pub openid_signing_key: Option, + #[arg(long, env = "DEFGUARD_HMAC")] + #[deprecated( + since = "2.0.0", + note = "Temporary compatibility flag for OpenID signing" + )] + pub hmac: Option, + #[arg(long, env = "DEFGUARD_URL", value_parser = Url::parse)] #[serde(skip_serializing)] #[deprecated(since = "2.0.0", note = "Use Settings.defguard_url instead")] @@ -261,6 +268,7 @@ impl DefGuardConfig { grpc_cert: None, grpc_key: None, openid_signing_key: None, + hmac: None, url: None, disable_stats_purge: None, stats_purge_frequency: None, @@ -362,15 +370,11 @@ mod tests { ); // only one flag at a time: must be an error - assert!( - make_config(Some("edge.example.com:8080"), None) - .validate_adopt_flags() - .is_err() - ); - assert!( - make_config(None, Some("gw.example.com:8080")) - .validate_adopt_flags() - .is_err() - ); + assert!(make_config(Some("edge.example.com:8080"), None) + .validate_adopt_flags() + .is_err()); + assert!(make_config(None, Some("gw.example.com:8080")) + .validate_adopt_flags() + .is_err()); } } diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index ef7bc17e1..bce6b0e8f 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -94,7 +94,7 @@ impl From<&UserClaims> for StandardClaims { pub async fn discovery_keys() -> ApiResult { let mut keys = Vec::new(); - if let Some(openid_key) = Settings::get_current_settings().openid_key() { + if let Some(openid_key) = runtime_openid_key() { keys.push(openid_key.as_verification_key()); } @@ -114,6 +114,15 @@ pub type DefguardIdTokenFields = IdTokenFields< pub type DefguardTokenResponse = StandardTokenResponse; pub struct OAuth2ClientExtractor(Option>); +#[allow(deprecated)] +fn runtime_openid_key() -> Option { + if server_config().hmac.unwrap_or(false) { + None + } else { + Settings::get_current_settings().openid_key() + } +} + /// Provide `OAuth2Client` when Basic Authorization header contains `client_id` and `client_secret`. impl FromRequestParts for OAuth2ClientExtractor where @@ -876,7 +885,7 @@ pub async fn token( }; let user_claims = UserClaims::from_user(&user, &client, &token); let base_url = Settings::url()?; - let openid_key = Settings::get_current_settings().openid_key(); + let openid_key = runtime_openid_key(); match form.authorization_code_flow( &auth_code, From 2161389242973bc6dfc5106d32c5fcbad8b56c9d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 11:50:41 +0200 Subject: [PATCH 04/22] rename migrations, remove secrets from debug impl --- crates/defguard_common/src/db/models/settings.rs | 5 ----- ...> 20260422113000_[2.0.0]_openid_signing_key_der.down.sql} | 0 ... => 20260422113000_[2.0.0]_openid_signing_key_der.up.sql} | 0 3 files changed, 5 deletions(-) rename migrations/{20260422153000_[2.0.0]_openid_signing_key_der.down.sql => 20260422113000_[2.0.0]_openid_signing_key_der.down.sql} (100%) rename migrations/{20260422153000_[2.0.0]_openid_signing_key_der.up.sql => 20260422113000_[2.0.0]_openid_signing_key_der.up.sql} (100%) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index d23cbc92d..8d992fad5 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -321,11 +321,6 @@ impl fmt::Debug for Settings { .field("mfa_code_timeout_seconds", &self.mfa_code_timeout_seconds) .field("public_proxy_url", &self.public_proxy_url) .field("default_admin_id", &self.default_admin_id) - .field("secret_key", &self.secret_key.as_ref().map(|_| "")) - .field( - "openid_signing_key_der", - &self.openid_signing_key_der.as_ref().map(Vec::len), - ) .finish_non_exhaustive() } } diff --git a/migrations/20260422153000_[2.0.0]_openid_signing_key_der.down.sql b/migrations/20260422113000_[2.0.0]_openid_signing_key_der.down.sql similarity index 100% rename from migrations/20260422153000_[2.0.0]_openid_signing_key_der.down.sql rename to migrations/20260422113000_[2.0.0]_openid_signing_key_der.down.sql diff --git a/migrations/20260422153000_[2.0.0]_openid_signing_key_der.up.sql b/migrations/20260422113000_[2.0.0]_openid_signing_key_der.up.sql similarity index 100% rename from migrations/20260422153000_[2.0.0]_openid_signing_key_der.up.sql rename to migrations/20260422113000_[2.0.0]_openid_signing_key_der.up.sql From 87fa1bdd4fba8ac3fa519fd07a7757b330125fd0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 11:58:21 +0200 Subject: [PATCH 05/22] return errors when key is unset --- crates/defguard_common/src/db/models/settings.rs | 14 ++++++++++++++ crates/defguard_core/src/handlers/openid_flow.rs | 13 ++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 8d992fad5..586f386cd 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -835,6 +835,20 @@ impl Settings { Ok(secret_key) } + pub fn openid_key_required(&self) -> Result { + let key_der = self + .openid_signing_key_der + .as_deref() + .ok_or(SettingsInitializationError::Missing("openid_signing_key_der"))?; + + Settings::validate_openid_signing_key_der(key_der)?; + + self.openid_key().ok_or(SettingsInitializationError::Invalid( + "openid_signing_key_der", + "failed to build OpenID signing key", + )) + } + pub fn proxy_public_url(&self) -> Result { Url::parse(&self.public_proxy_url) } diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index bce6b0e8f..563543e8b 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -94,7 +94,7 @@ impl From<&UserClaims> for StandardClaims { pub async fn discovery_keys() -> ApiResult { let mut keys = Vec::new(); - if let Some(openid_key) = runtime_openid_key() { + if let Some(openid_key) = runtime_openid_key()? { keys.push(openid_key.as_verification_key()); } @@ -115,11 +115,14 @@ pub type DefguardTokenResponse = StandardTokenResponse>); #[allow(deprecated)] -fn runtime_openid_key() -> Option { +fn runtime_openid_key() -> Result, WebError> { if server_config().hmac.unwrap_or(false) { - None + Ok(None) } else { - Settings::get_current_settings().openid_key() + Settings::get_current_settings() + .openid_key_required() + .map(Some) + .map_err(|err| WebError::BadRequest(err.to_string())) } } @@ -885,7 +888,7 @@ pub async fn token( }; let user_claims = UserClaims::from_user(&user, &client, &token); let base_url = Settings::url()?; - let openid_key = runtime_openid_key(); + let openid_key = runtime_openid_key()?; match form.authorization_code_flow( &auth_code, From c70c39427079f26db3961c5566442e7756401897 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 12:07:26 +0200 Subject: [PATCH 06/22] add tests --- .../defguard_common/src/db/models/settings.rs | 33 ++++++++ .../tests/integration/api/openid.rs | 75 +++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 586f386cd..64b0d28e2 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -1411,6 +1411,39 @@ mod test { assert_eq!(settings.openid_signing_key_der, Some(existing)); } + #[test] + fn test_openid_key_required_rejects_missing_key() { + let settings = Settings::default(); + + assert!(matches!( + settings.openid_key_required(), + Err(SettingsInitializationError::Missing("openid_signing_key_der")) + )); + } + + #[test] + fn test_openid_key_required_rejects_invalid_der() { + let settings = Settings { + openid_signing_key_der: Some(vec![1, 2, 3]), + ..Default::default() + }; + + assert!(matches!( + settings.openid_key_required(), + Err(SettingsInitializationError::Invalid("openid_signing_key_der", _)) + )); + } + + #[test] + fn test_openid_key_required_accepts_valid_der() { + let settings = Settings { + openid_signing_key_der: Some(Settings::generate_openid_signing_key_der()), + ..Default::default() + }; + + assert!(settings.openid_key_required().is_ok()); + } + #[sqlx::test] #[allow(deprecated)] async fn test_update_from_config_persists_and_updates_current_settings( diff --git a/crates/defguard_core/tests/integration/api/openid.rs b/crates/defguard_core/tests/integration/api/openid.rs index 1a798d535..9397834a7 100644 --- a/crates/defguard_core/tests/integration/api/openid.rs +++ b/crates/defguard_core/tests/integration/api/openid.rs @@ -584,6 +584,81 @@ async fn test_openid_authorization_code(_: PgPoolOptions, options: PgConnectOpti assert!(refresh_response.refresh_token().is_some()); } +#[sqlx::test] +async fn test_openid_flow_fails_when_rsa_key_is_missing_and_hmac_is_not_forced( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let (client, _) = make_test_client(pool).await; + + let mut settings = Settings::get_current_settings(); + settings.openid_signing_key_der = None; + update_current_settings(&pool, settings).await.unwrap(); + + let issuer_url = IssuerUrl::from_url(Settings::url().unwrap().clone()); + let provider_metadata = + CoreProviderMetadata::discover_async(issuer_url, &|r| http_client(r, &client)) + .await + .unwrap(); + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + let oauth2client = NewOpenIDClient { + name: "My test client".into(), + redirect_uri: vec![FAKE_REDIRECT_URI.into()], + scope: vec!["openid".into()], + enabled: true, + }; + let response = client + .post("/api/v1/oauth") + .json(&oauth2client) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let oauth2client: OAuth2Client = response.json().await; + + let client_id = ClientId::new(oauth2client.client_id); + let client_secret = ClientSecret::new(oauth2client.client_secret); + let core_client = + CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) + .set_redirect_uri(RedirectUrl::new(FAKE_REDIRECT_URI.into()).unwrap()); + let (authorize_url, _csrf_state, _nonce) = core_client + .authorize_url( + AuthenticationFlow::::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .url(); + + let uri = format!( + "{}?allow=true&{}", + authorize_url.path(), + authorize_url.query().unwrap() + ); + let response = client.post(uri).send().await; + assert_eq!(response.status(), StatusCode::FOUND); + let location = response + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap(); + let (_location, query) = location.split_once('?').unwrap(); + let auth_response: AuthenticationResponse = serde_qs::from_str(query).unwrap(); + + let token_response = core_client + .exchange_code(AuthorizationCode::new(auth_response.code.into())) + .unwrap() + .request_async(&|r| http_client(r, &client)) + .await; + + assert!(token_response.is_err()); +} + #[sqlx::test] async fn dg25_20_test_openid_disabled_client_doesnt_generate_code( _: PgPoolOptions, From 459b336d3768244242ea668a645971bbfdca3104 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 12:23:13 +0200 Subject: [PATCH 07/22] fix settings access order --- crates/defguard/src/main.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index d6ddd176b..39c9bf5df 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -94,15 +94,6 @@ async fn main() -> Result<(), anyhow::Error> { ) .await; - #[allow(deprecated)] - if config.hmac.unwrap_or(false) { - info!("Using HMAC OpenID signing key (forced by deprecated config flag)"); - } else if Settings::get_current_settings().openid_key().is_some() { - info!("Using RSA OpenID signing key"); - } else { - info!("Using HMAC OpenID signing key"); - } - // initialize global settings struct initialize_current_settings(&pool).await?; @@ -199,6 +190,15 @@ async fn main() -> Result<(), anyhow::Error> { let settings = Settings::get_current_settings(); + #[allow(deprecated)] + if config.hmac.unwrap_or(false) { + info!("Using HMAC OpenID signing key (forced by deprecated config flag)"); + } else if settings.openid_key().is_some() { + info!("Using RSA OpenID signing key"); + } else { + info!("Using HMAC OpenID signing key"); + } + // create event channels for services let (api_event_tx, api_event_rx) = unbounded_channel::(); let (bidi_event_tx, bidi_event_rx) = unbounded_channel::(); From 88dd050526fe29cf09d6ea6c483cea28ed0408a2 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 12:53:53 +0200 Subject: [PATCH 08/22] default value for hmac flag --- crates/defguard/src/main.rs | 6 ++---- crates/defguard_common/src/config.rs | 6 +++--- crates/defguard_core/src/handlers/openid_flow.rs | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 39c9bf5df..6f78904b9 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -191,12 +191,10 @@ async fn main() -> Result<(), anyhow::Error> { let settings = Settings::get_current_settings(); #[allow(deprecated)] - if config.hmac.unwrap_or(false) { + if config.hmac { info!("Using HMAC OpenID signing key (forced by deprecated config flag)"); - } else if settings.openid_key().is_some() { - info!("Using RSA OpenID signing key"); } else { - info!("Using HMAC OpenID signing key"); + info!("Using RSA OpenID signing key"); } // create event channels for services diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index 52bac0213..4c36ac223 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -74,12 +74,12 @@ pub struct DefGuardConfig { #[serde(skip_serializing)] pub openid_signing_key: Option, - #[arg(long, env = "DEFGUARD_HMAC")] + #[arg(long, env = "DEFGUARD_HMAC", default_value_t = false)] #[deprecated( since = "2.0.0", note = "Temporary compatibility flag for OpenID signing" )] - pub hmac: Option, + pub hmac: bool, #[arg(long, env = "DEFGUARD_URL", value_parser = Url::parse)] #[serde(skip_serializing)] @@ -268,7 +268,7 @@ impl DefGuardConfig { grpc_cert: None, grpc_key: None, openid_signing_key: None, - hmac: None, + hmac: false, url: None, disable_stats_purge: None, stats_purge_frequency: None, diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index 563543e8b..d2fe98d25 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -116,7 +116,7 @@ pub struct OAuth2ClientExtractor(Option>); #[allow(deprecated)] fn runtime_openid_key() -> Result, WebError> { - if server_config().hmac.unwrap_or(false) { + if server_config().hmac { Ok(None) } else { Settings::get_current_settings() From 0d2e53870c3c87422feb591fe8e046b67f037548 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 12:54:46 +0200 Subject: [PATCH 09/22] cargo fmt --- crates/defguard_common/src/config.rs | 22 ++++++---- .../defguard_common/src/db/models/settings.rs | 41 +++++++++++-------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index 4c36ac223..bb26422e0 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -3,18 +3,18 @@ use std::{net::IpAddr, sync::OnceLock}; use clap::{Args, Parser, Subcommand}; use humantime::Duration; use ipnetwork::IpNetwork; -use openidconnect::{core::CoreRsaPrivateSigningKey, JsonWebKeyId}; +use openidconnect::{JsonWebKeyId, core::CoreRsaPrivateSigningKey}; use reqwest::Url; use rsa::{ + RsaPrivateKey, pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey}, pkcs8::{DecodePrivateKey, LineEnding}, traits::PublicKeyParts, - RsaPrivateKey, }; use secrecy::{ExposeSecret, SecretString}; use serde::Serialize; -use crate::{db::models::Settings, VERSION}; +use crate::{VERSION, db::models::Settings}; pub static SERVER_CONFIG: OnceLock = OnceLock::new(); @@ -370,11 +370,15 @@ mod tests { ); // only one flag at a time: must be an error - assert!(make_config(Some("edge.example.com:8080"), None) - .validate_adopt_flags() - .is_err()); - assert!(make_config(None, Some("gw.example.com:8080")) - .validate_adopt_flags() - .is_err()); + assert!( + make_config(Some("edge.example.com:8080"), None) + .validate_adopt_flags() + .is_err() + ); + assert!( + make_config(None, Some("gw.example.com:8080")) + .validate_adopt_flags() + .is_err() + ); } } diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 64b0d28e2..99165cd3d 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -1,15 +1,15 @@ use std::{collections::HashMap, fmt, net::IpAddr, time::Duration}; use base64::{Engine, prelude::BASE64_STANDARD}; +use openidconnect::{JsonWebKeyId, core::CoreRsaPrivateSigningKey}; use rand::{RngCore, rngs::OsRng}; -use openidconnect::{core::CoreRsaPrivateSigningKey, JsonWebKeyId}; +use rsa::pkcs8::LineEnding; use rsa::{ RsaPrivateKey, pkcs1::EncodeRsaPrivateKey, pkcs8::{DecodePrivateKey, EncodePrivateKey}, traits::PublicKeyParts, }; -use rsa::pkcs8::LineEnding; use secrecy::ExposeSecret; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, Type, query, query_as}; @@ -722,8 +722,7 @@ impl Settings { Settings::validate_openid_signing_key_der(key_der)?; } None => { - settings.openid_signing_key_der = - Some(Settings::generate_openid_signing_key_der()); + settings.openid_signing_key_der = Some(Settings::generate_openid_signing_key_der()); } } @@ -835,18 +834,23 @@ impl Settings { Ok(secret_key) } - pub fn openid_key_required(&self) -> Result { - let key_der = self - .openid_signing_key_der - .as_deref() - .ok_or(SettingsInitializationError::Missing("openid_signing_key_der"))?; + pub fn openid_key_required( + &self, + ) -> Result { + let key_der = + self.openid_signing_key_der + .as_deref() + .ok_or(SettingsInitializationError::Missing( + "openid_signing_key_der", + ))?; Settings::validate_openid_signing_key_der(key_der)?; - self.openid_key().ok_or(SettingsInitializationError::Invalid( - "openid_signing_key_der", - "failed to build OpenID signing key", - )) + self.openid_key() + .ok_or(SettingsInitializationError::Invalid( + "openid_signing_key_der", + "failed to build OpenID signing key", + )) } pub fn proxy_public_url(&self) -> Result { @@ -1046,8 +1050,8 @@ mod test { use std::str::FromStr; use humantime::Duration; - use rsa::{RsaPrivateKey, pkcs8::EncodePrivateKey}; use reqwest::Url; + use rsa::{RsaPrivateKey, pkcs8::EncodePrivateKey}; use secrecy::SecretString; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; @@ -1417,7 +1421,9 @@ mod test { assert!(matches!( settings.openid_key_required(), - Err(SettingsInitializationError::Missing("openid_signing_key_der")) + Err(SettingsInitializationError::Missing( + "openid_signing_key_der" + )) )); } @@ -1430,7 +1436,10 @@ mod test { assert!(matches!( settings.openid_key_required(), - Err(SettingsInitializationError::Invalid("openid_signing_key_der", _)) + Err(SettingsInitializationError::Invalid( + "openid_signing_key_der", + _ + )) )); } From ed1ee7eac9d3a6418eac752f378447b1ccd56f04 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:03:58 +0200 Subject: [PATCH 10/22] comments, deprecation attrs --- crates/defguard_common/src/config.rs | 8 ++++++++ crates/defguard_common/src/db/models/settings.rs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index bb26422e0..4373139a9 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -71,6 +71,10 @@ pub struct DefGuardConfig { pub grpc_key: Option, #[arg(long, env = "DEFGUARD_OPENID_KEY", value_parser = Self::parse_openid_key)] + #[deprecated( + since = "2.0.0", + note = "Use auto-generated openid signing key" + )] #[serde(skip_serializing)] pub openid_signing_key: Option, @@ -323,6 +327,10 @@ impl DefGuardConfig { } #[must_use] + #[deprecated( + since = "2.0.0", + note = "Use auto-generated openid signing key" + )] pub fn openid_key(&self) -> Option { let key = self.openid_signing_key.as_ref()?; if let Ok(pem) = key.to_pkcs1_pem(LineEnding::default()) { diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 99165cd3d..60dfdc615 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -351,6 +351,7 @@ impl Settings { BASE64_STANDARD.encode(bytes) } + /// Generates a new RSA private key for OpenID signing and serializes it as PKCS#8 DER. fn generate_openid_signing_key_der() -> Vec { RsaPrivateKey::new(&mut OsRng, 2048) .expect("failed to generate OpenID signing key") @@ -371,6 +372,7 @@ impl Settings { }) } + /// Serializes the deprecated config-provided OpenID RSA key as PKCS#8 DER for storage. fn openid_signing_key_der_from_config( key: &RsaPrivateKey, ) -> Result, SettingsInitializationError> { @@ -384,6 +386,7 @@ impl Settings { }) } + /// Builds the runtime OpenID signing key from the stored DER-encoded private key. #[must_use] pub fn openid_key(&self) -> Option { let key_der = self.openid_signing_key_der.as_deref()?; From 6d3bccd593ecc1a94a599877abcfa441ec16e746 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:06:53 +0200 Subject: [PATCH 11/22] sqlx fixtures --- ...ab41ede332cf22af3fa9ac264c9c9b6a47f2.json} | 22 ++++++++++++------- ...263ddcbdf3d3b4803d63d6f8ecb986cb22d0.json} | 5 +++-- crates/defguard_common/src/config.rs | 1 + 3 files changed, 18 insertions(+), 10 deletions(-) rename .sqlx/{query-fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab.json => query-94632716a5f1088ff341bd74a48aab41ede332cf22af3fa9ac264c9c9b6a47f2.json} (95%) rename .sqlx/{query-01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20.json => query-e65193de91eb08866f0303a4a842263ddcbdf3d3b4803d63d6f8ecb986cb22d0.json} (88%) diff --git a/.sqlx/query-fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab.json b/.sqlx/query-94632716a5f1088ff341bd74a48aab41ede332cf22af3fa9ac264c9c9b6a47f2.json similarity index 95% rename from .sqlx/query-fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab.json rename to .sqlx/query-94632716a5f1088ff341bd74a48aab41ede332cf22af3fa9ac264c9c9b6a47f2.json index 4285c6ab8..911ec5e31 100644 --- a/.sqlx/query-fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab.json +++ b/.sqlx/query-94632716a5f1088ff341bd74a48aab41ede332cf22af3fa9ac264c9c9b6a47f2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, enrollment_send_welcome_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, ldap_remote_enrollment_enabled, ldap_remote_enrollment_send_invite, openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", defguard_url, default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, public_proxy_url, default_admin_id, secret_key, enable_stats_purge, stats_purge_frequency_hours, stats_purge_threshold_days, enrollment_token_timeout_hours, password_reset_token_timeout_hours, enrollment_session_timeout_minutes, password_reset_session_timeout_minutes FROM \"settings\" WHERE id = 1", + "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, enrollment_send_welcome_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, ldap_remote_enrollment_enabled, ldap_remote_enrollment_send_invite, openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", defguard_url, default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, public_proxy_url, default_admin_id, secret_key, openid_signing_key_der, enable_stats_purge, stats_purge_frequency_hours, stats_purge_threshold_days, enrollment_token_timeout_hours, password_reset_token_timeout_hours, enrollment_session_timeout_minutes, password_reset_session_timeout_minutes FROM \"settings\" WHERE id = 1", "describe": { "columns": [ { @@ -327,36 +327,41 @@ }, { "ordinal": 58, + "name": "openid_signing_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 59, "name": "enable_stats_purge", "type_info": "Bool" }, { - "ordinal": 59, + "ordinal": 60, "name": "stats_purge_frequency_hours", "type_info": "Int4" }, { - "ordinal": 60, + "ordinal": 61, "name": "stats_purge_threshold_days", "type_info": "Int4" }, { - "ordinal": 61, + "ordinal": 62, "name": "enrollment_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 62, + "ordinal": 63, "name": "password_reset_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 63, + "ordinal": 64, "name": "enrollment_session_timeout_minutes", "type_info": "Int4" }, { - "ordinal": 64, + "ordinal": 65, "name": "password_reset_session_timeout_minutes", "type_info": "Int4" } @@ -423,6 +428,7 @@ false, true, true, + true, false, false, false, @@ -432,5 +438,5 @@ false ] }, - "hash": "fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab" + "hash": "94632716a5f1088ff341bd74a48aab41ede332cf22af3fa9ac264c9c9b6a47f2" } diff --git a/.sqlx/query-01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20.json b/.sqlx/query-e65193de91eb08866f0303a4a842263ddcbdf3d3b4803d63d6f8ecb986cb22d0.json similarity index 88% rename from .sqlx/query-01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20.json rename to .sqlx/query-e65193de91eb08866f0303a4a842263ddcbdf3d3b4803d63d6f8ecb986cb22d0.json index b6add23ae..539d9403a 100644 --- a/.sqlx/query-01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20.json +++ b/.sqlx/query-e65193de91eb08866f0303a4a842263ddcbdf3d3b4803d63d6f8ecb986cb22d0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, enrollment_send_welcome_email = $20, uuid = $21, ldap_url = $22, ldap_bind_username = $23, ldap_bind_password = $24, ldap_group_search_base = $25, ldap_user_search_base = $26, ldap_user_obj_class = $27, ldap_group_obj_class = $28, ldap_username_attr = $29, ldap_groupname_attr = $30, ldap_group_member_attr = $31, ldap_member_attr = $32, ldap_use_starttls = $33, ldap_tls_verify_cert = $34, openid_create_account = $35, license = $36, gateway_disconnect_notifications_enabled = $37, gateway_disconnect_notifications_inactivity_threshold = $38, gateway_disconnect_notifications_reconnect_notification_enabled = $39, ldap_sync_status = $40, ldap_enabled = $41, ldap_sync_enabled = $42, ldap_is_authoritative = $43, ldap_sync_interval = $44, ldap_user_auxiliary_obj_classes = $45, ldap_uses_ad = $46, ldap_user_rdn_attr = $47, ldap_sync_groups = $48, ldap_remote_enrollment_enabled = $49, ldap_remote_enrollment_send_invite = $50, openid_username_handling = $51, defguard_url = $52, default_admin_group_name = $53, authentication_period_days = $54, mfa_code_timeout_seconds = $55, public_proxy_url = $56, default_admin_id = $57, secret_key = $58, enable_stats_purge = $59, stats_purge_frequency_hours = $60, stats_purge_threshold_days = $61, enrollment_token_timeout_hours = $62, password_reset_token_timeout_hours = $63, enrollment_session_timeout_minutes = $64, password_reset_session_timeout_minutes = $65 WHERE id = 1", + "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, enrollment_send_welcome_email = $20, uuid = $21, ldap_url = $22, ldap_bind_username = $23, ldap_bind_password = $24, ldap_group_search_base = $25, ldap_user_search_base = $26, ldap_user_obj_class = $27, ldap_group_obj_class = $28, ldap_username_attr = $29, ldap_groupname_attr = $30, ldap_group_member_attr = $31, ldap_member_attr = $32, ldap_use_starttls = $33, ldap_tls_verify_cert = $34, openid_create_account = $35, license = $36, gateway_disconnect_notifications_enabled = $37, gateway_disconnect_notifications_inactivity_threshold = $38, gateway_disconnect_notifications_reconnect_notification_enabled = $39, ldap_sync_status = $40, ldap_enabled = $41, ldap_sync_enabled = $42, ldap_is_authoritative = $43, ldap_sync_interval = $44, ldap_user_auxiliary_obj_classes = $45, ldap_uses_ad = $46, ldap_user_rdn_attr = $47, ldap_sync_groups = $48, ldap_remote_enrollment_enabled = $49, ldap_remote_enrollment_send_invite = $50, openid_username_handling = $51, defguard_url = $52, default_admin_group_name = $53, authentication_period_days = $54, mfa_code_timeout_seconds = $55, public_proxy_url = $56, default_admin_id = $57, secret_key = $58, openid_signing_key_der = $59, enable_stats_purge = $60, stats_purge_frequency_hours = $61, stats_purge_threshold_days = $62, enrollment_token_timeout_hours = $63, password_reset_token_timeout_hours = $64, enrollment_session_timeout_minutes = $65, password_reset_session_timeout_minutes = $66 WHERE id = 1", "describe": { "columns": [], "parameters": { @@ -95,6 +95,7 @@ "Text", "Int8", "Text", + "Bytea", "Bool", "Int4", "Int4", @@ -106,5 +107,5 @@ }, "nullable": [] }, - "hash": "01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20" + "hash": "e65193de91eb08866f0303a4a842263ddcbdf3d3b4803d63d6f8ecb986cb22d0" } diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index 4373139a9..82d8a3068 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -332,6 +332,7 @@ impl DefGuardConfig { note = "Use auto-generated openid signing key" )] pub fn openid_key(&self) -> Option { + #[allow(deprecated)] let key = self.openid_signing_key.as_ref()?; if let Ok(pem) = key.to_pkcs1_pem(LineEnding::default()) { let key_id = JsonWebKeyId::new(key.n().to_str_radix(36)); From 40e5c6422d5bc5d9cb10f67e6d5d0b80ae83cd4d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:15:27 +0200 Subject: [PATCH 12/22] fix tests --- crates/defguard_common/src/db/models/settings.rs | 1 + crates/defguard_core/tests/integration/api/openid.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 60dfdc615..d3e0cf6e5 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -1389,6 +1389,7 @@ mod test { } #[test] + #[allow(deprecated)] fn test_apply_from_config_valid_openid_signing_key_overwrites_existing_value() { let mut settings = Settings { openid_signing_key_der: Some(Settings::generate_openid_signing_key_der()), diff --git a/crates/defguard_core/tests/integration/api/openid.rs b/crates/defguard_core/tests/integration/api/openid.rs index 9397834a7..8ad7e3e1f 100644 --- a/crates/defguard_core/tests/integration/api/openid.rs +++ b/crates/defguard_core/tests/integration/api/openid.rs @@ -22,7 +22,7 @@ use reqwest::{ StatusCode, Url, header::{AUTHORIZATION, CONTENT_TYPE, HeaderName, LOCATION, USER_AGENT}, }; -use rsa::RsaPrivateKey; +use rsa::{RsaPrivateKey, pkcs8::EncodePrivateKey}; use serde::Deserialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; @@ -118,7 +118,7 @@ async fn test_openid_client(_: PgPoolOptions, options: PgConnectOptions) { async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; + let (client, _) = make_test_client(pool.clone()).await; let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); @@ -487,7 +487,7 @@ static FAKE_REDIRECT_URI: &str = "http://test.server.tnt:12345/"; async fn test_openid_authorization_code(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; + let (client, _) = make_test_client(pool.clone()).await; let issuer_url = IssuerUrl::from_url(Settings::url().unwrap().clone()); @@ -591,7 +591,7 @@ async fn test_openid_flow_fails_when_rsa_key_is_missing_and_hmac_is_not_forced( ) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; + let (client, _) = make_test_client(pool.clone()).await; let mut settings = Settings::get_current_settings(); settings.openid_signing_key_der = None; From f1f0b9fcae183b2a4ed98a133e2c05405bb46982 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:17:38 +0200 Subject: [PATCH 13/22] cargo fmt --- crates/defguard_common/src/config.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index 82d8a3068..77ac0dfc8 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -71,10 +71,7 @@ pub struct DefGuardConfig { pub grpc_key: Option, #[arg(long, env = "DEFGUARD_OPENID_KEY", value_parser = Self::parse_openid_key)] - #[deprecated( - since = "2.0.0", - note = "Use auto-generated openid signing key" - )] + #[deprecated(since = "2.0.0", note = "Use auto-generated openid signing key")] #[serde(skip_serializing)] pub openid_signing_key: Option, @@ -327,10 +324,7 @@ impl DefGuardConfig { } #[must_use] - #[deprecated( - since = "2.0.0", - note = "Use auto-generated openid signing key" - )] + #[deprecated(since = "2.0.0", note = "Use auto-generated openid signing key")] pub fn openid_key(&self) -> Option { #[allow(deprecated)] let key = self.openid_signing_key.as_ref()?; From 8bb84b872b552af9b4c5d79a1820ee104d43ae8c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:25:28 +0200 Subject: [PATCH 14/22] docstring --- crates/defguard_common/src/db/models/settings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index d3e0cf6e5..d68b6252c 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -837,6 +837,7 @@ impl Settings { Ok(secret_key) } + /// Builds the runtime OpenID signing key from stored DER bytes or returns an error if missing or invalid. pub fn openid_key_required( &self, ) -> Result { From 07cb6aa06d72c00a9d38b0bbcc87cb07b8455f95 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:38:05 +0200 Subject: [PATCH 15/22] fix tests --- .../defguard_core/tests/integration/api/common/mod.rs | 1 + crates/defguard_core/tests/integration/common.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index f514e313b..97e219a32 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -83,6 +83,7 @@ impl ClientState { pub(crate) async fn make_base_client( pool: PgPool, + #[allow(unused_variables)] config: DefGuardConfig, listener: TcpListener, ) -> (TestClient, ClientState) { diff --git a/crates/defguard_core/tests/integration/common.rs b/crates/defguard_core/tests/integration/common.rs index ec07ae227..b0e580fc9 100644 --- a/crates/defguard_core/tests/integration/common.rs +++ b/crates/defguard_core/tests/integration/common.rs @@ -2,7 +2,9 @@ use defguard_common::{ config::{DefGuardConfig, SERVER_CONFIG}, db::models::{ Settings, User, - settings::{initialize_current_settings, update_current_settings}, + settings::{ + SettingsInitializationError, initialize_current_settings, update_current_settings, + }, }, }; use defguard_core::enterprise::license::{License, LicenseTier, SupportType, set_cached_license}; @@ -39,6 +41,13 @@ pub(crate) async fn init_config( update_current_settings(pool, settings) .await .expect("Could not update current settings in the database"); + Settings::initialize_runtime_defaults(pool) + .await + .map_err(|err| match err { + SettingsInitializationError::Save(err) => err, + other => panic!("Could not initialize runtime default settings in the database: {other}"), + }) + .expect("Could not initialize runtime default settings in the database"); set_test_license_business(); let _ = SERVER_CONFIG.set(config.clone()); From f9262f2209743c10fa2fc8496f4ce6515d32796b Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:42:25 +0200 Subject: [PATCH 16/22] don't expect --- .../defguard_common/src/db/models/settings.rs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index d68b6252c..bcf3df548 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -352,13 +352,22 @@ impl Settings { } /// Generates a new RSA private key for OpenID signing and serializes it as PKCS#8 DER. - fn generate_openid_signing_key_der() -> Vec { - RsaPrivateKey::new(&mut OsRng, 2048) - .expect("failed to generate OpenID signing key") - .to_pkcs8_der() - .expect("failed to serialize OpenID signing key") - .as_bytes() - .to_vec() + fn generate_openid_signing_key_der() -> Result, SettingsInitializationError> { + let key = RsaPrivateKey::new(&mut OsRng, 2048).map_err(|_| { + SettingsInitializationError::Invalid( + "openid_signing_key_der", + "failed to generate OpenID signing key", + ) + })?; + + let key_der = key.to_pkcs8_der().map_err(|_| { + SettingsInitializationError::Invalid( + "openid_signing_key_der", + "failed to serialize OpenID signing key", + ) + })?; + + Ok(key_der.as_bytes().to_vec()) } fn validate_openid_signing_key_der(key_der: &[u8]) -> Result<(), SettingsInitializationError> { @@ -725,7 +734,8 @@ impl Settings { Settings::validate_openid_signing_key_der(key_der)?; } None => { - settings.openid_signing_key_der = Some(Settings::generate_openid_signing_key_der()); + settings.openid_signing_key_der = + Some(Settings::generate_openid_signing_key_der()?); } } @@ -905,7 +915,8 @@ impl Settings { warn!( "Invalid openid_signing_key provided in deprecated config, generating new one: {err}" ); - self.openid_signing_key_der = Some(Settings::generate_openid_signing_key_der()); + self.openid_signing_key_der = + Settings::generate_openid_signing_key_der().ok(); } } } @@ -1393,7 +1404,7 @@ mod test { #[allow(deprecated)] fn test_apply_from_config_valid_openid_signing_key_overwrites_existing_value() { let mut settings = Settings { - openid_signing_key_der: Some(Settings::generate_openid_signing_key_der()), + openid_signing_key_der: Some(Settings::generate_openid_signing_key_der().unwrap()), ..Default::default() }; let mut config = DefGuardConfig::new_test_config(); @@ -1408,7 +1419,7 @@ mod test { #[test] fn test_apply_from_config_keeps_openid_signing_key_when_config_is_none() { - let existing = Settings::generate_openid_signing_key_der(); + let existing = Settings::generate_openid_signing_key_der().unwrap(); let mut settings = Settings { openid_signing_key_der: Some(existing.clone()), ..Default::default() @@ -1451,7 +1462,7 @@ mod test { #[test] fn test_openid_key_required_accepts_valid_der() { let settings = Settings { - openid_signing_key_der: Some(Settings::generate_openid_signing_key_der()), + openid_signing_key_der: Some(Settings::generate_openid_signing_key_der().unwrap()), ..Default::default() }; From 48b2d6f572bad86a87e7153f62ca523378ecfdcc Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:47:05 +0200 Subject: [PATCH 17/22] return http 500 when rsa key is missing --- crates/defguard_core/src/handlers/openid_flow.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index d2fe98d25..54742d396 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -122,7 +122,10 @@ fn runtime_openid_key() -> Result, WebError> { Settings::get_current_settings() .openid_key_required() .map(Some) - .map_err(|err| WebError::BadRequest(err.to_string())) + .map_err(|err| { + error!("OpenID signing key is unavailable: {err}"); + WebError::Http(StatusCode::INTERNAL_SERVER_ERROR) + }) } } From b1d8dd3b92bd147d96f5831818a1c211f83d55bd Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 13:51:16 +0200 Subject: [PATCH 18/22] skip serializing openid_signing_key --- crates/defguard_common/src/db/models/settings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index bcf3df548..06709709b 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -223,6 +223,7 @@ pub struct Settings { pub default_admin_id: Option, // 1.6 config options pub secret_key: Option, + #[serde(skip)] pub openid_signing_key_der: Option>, pub enable_stats_purge: bool, stats_purge_frequency_hours: i32, From 47539fb40f5b6dc73cea4b0ac1c286179b4dd963 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 14:05:48 +0200 Subject: [PATCH 19/22] cargo fmt --- crates/defguard_common/src/db/models/settings.rs | 3 +-- crates/defguard_core/tests/integration/api/common/mod.rs | 2 +- crates/defguard_core/tests/integration/common.rs | 4 +++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 06709709b..dd31cda8a 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -916,8 +916,7 @@ impl Settings { warn!( "Invalid openid_signing_key provided in deprecated config, generating new one: {err}" ); - self.openid_signing_key_der = - Settings::generate_openid_signing_key_der().ok(); + self.openid_signing_key_der = Settings::generate_openid_signing_key_der().ok(); } } } diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index 97e219a32..56d20f666 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -60,6 +60,7 @@ pub(crate) struct ClientState { pub worker_state: Arc>, pub wireguard_rx: Receiver, pub test_user: User, + #[allow(dead_code)] pub config: DefGuardConfig, } @@ -83,7 +84,6 @@ impl ClientState { pub(crate) async fn make_base_client( pool: PgPool, - #[allow(unused_variables)] config: DefGuardConfig, listener: TcpListener, ) -> (TestClient, ClientState) { diff --git a/crates/defguard_core/tests/integration/common.rs b/crates/defguard_core/tests/integration/common.rs index b0e580fc9..5cdbd6aef 100644 --- a/crates/defguard_core/tests/integration/common.rs +++ b/crates/defguard_core/tests/integration/common.rs @@ -45,7 +45,9 @@ pub(crate) async fn init_config( .await .map_err(|err| match err { SettingsInitializationError::Save(err) => err, - other => panic!("Could not initialize runtime default settings in the database: {other}"), + other => { + panic!("Could not initialize runtime default settings in the database: {other}") + } }) .expect("Could not initialize runtime default settings in the database"); set_test_license_business(); From 867a33ddd933e389dd2bd30d118fbe0cf60a431c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 14:08:36 +0200 Subject: [PATCH 20/22] fix test --- .../tests/integration/api/openid.rs | 44 ++----------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/crates/defguard_core/tests/integration/api/openid.rs b/crates/defguard_core/tests/integration/api/openid.rs index 8ad7e3e1f..27014b0ad 100644 --- a/crates/defguard_core/tests/integration/api/openid.rs +++ b/crates/defguard_core/tests/integration/api/openid.rs @@ -599,9 +599,9 @@ async fn test_openid_flow_fails_when_rsa_key_is_missing_and_hmac_is_not_forced( let issuer_url = IssuerUrl::from_url(Settings::url().unwrap().clone()); let provider_metadata = - CoreProviderMetadata::discover_async(issuer_url, &|r| http_client(r, &client)) - .await - .unwrap(); + CoreProviderMetadata::discover_async(issuer_url, &|r| http_client(r, &client)).await; + + assert!(provider_metadata.is_err()); let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; @@ -619,44 +619,6 @@ async fn test_openid_flow_fails_when_rsa_key_is_missing_and_hmac_is_not_forced( .send() .await; assert_eq!(response.status(), StatusCode::CREATED); - let oauth2client: OAuth2Client = response.json().await; - - let client_id = ClientId::new(oauth2client.client_id); - let client_secret = ClientSecret::new(oauth2client.client_secret); - let core_client = - CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) - .set_redirect_uri(RedirectUrl::new(FAKE_REDIRECT_URI.into()).unwrap()); - let (authorize_url, _csrf_state, _nonce) = core_client - .authorize_url( - AuthenticationFlow::::AuthorizationCode, - CsrfToken::new_random, - Nonce::new_random, - ) - .url(); - - let uri = format!( - "{}?allow=true&{}", - authorize_url.path(), - authorize_url.query().unwrap() - ); - let response = client.post(uri).send().await; - assert_eq!(response.status(), StatusCode::FOUND); - let location = response - .headers() - .get("Location") - .unwrap() - .to_str() - .unwrap(); - let (_location, query) = location.split_once('?').unwrap(); - let auth_response: AuthenticationResponse = serde_qs::from_str(query).unwrap(); - - let token_response = core_client - .exchange_code(AuthorizationCode::new(auth_response.code.into())) - .unwrap() - .request_async(&|r| http_client(r, &client)) - .await; - - assert!(token_response.is_err()); } #[sqlx::test] From 5367416453307e74b8d65c2da95744740c771116 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 14:14:44 +0200 Subject: [PATCH 21/22] merge imports --- crates/defguard_common/src/db/models/settings.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index dd31cda8a..540d3d026 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -3,11 +3,10 @@ use std::{collections::HashMap, fmt, net::IpAddr, time::Duration}; use base64::{Engine, prelude::BASE64_STANDARD}; use openidconnect::{JsonWebKeyId, core::CoreRsaPrivateSigningKey}; use rand::{RngCore, rngs::OsRng}; -use rsa::pkcs8::LineEnding; use rsa::{ RsaPrivateKey, pkcs1::EncodeRsaPrivateKey, - pkcs8::{DecodePrivateKey, EncodePrivateKey}, + pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding}, traits::PublicKeyParts, }; use secrecy::ExposeSecret; From 315a5acaa772f7d1f4d04c1582d669bb2dc4739d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 22 Apr 2026 14:24:09 +0200 Subject: [PATCH 22/22] openid key size const --- crates/defguard_common/src/db/models/settings.rs | 5 +++-- crates/defguard_core/tests/integration/api/openid.rs | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 540d3d026..514c9747e 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -25,6 +25,7 @@ use crate::{ }; global_value!(SETTINGS, Option, None, set_settings, get_settings); +pub const OPENID_KEY_SIZE: usize = 2048; /// Initializes global `SETTINGS` struct at program startup pub async fn initialize_current_settings(pool: &PgPool) -> sqlx::Result<()> { @@ -353,7 +354,7 @@ impl Settings { /// Generates a new RSA private key for OpenID signing and serializes it as PKCS#8 DER. fn generate_openid_signing_key_der() -> Result, SettingsInitializationError> { - let key = RsaPrivateKey::new(&mut OsRng, 2048).map_err(|_| { + let key = RsaPrivateKey::new(&mut OsRng, OPENID_KEY_SIZE).map_err(|_| { SettingsInitializationError::Invalid( "openid_signing_key_der", "failed to generate OpenID signing key", @@ -1407,7 +1408,7 @@ mod test { ..Default::default() }; let mut config = DefGuardConfig::new_test_config(); - let configured_key = RsaPrivateKey::new(&mut OsRng, 2048).unwrap(); + let configured_key = RsaPrivateKey::new(&mut OsRng, OPENID_KEY_SIZE).unwrap(); let expected_der = configured_key.to_pkcs8_der().unwrap().as_bytes().to_vec(); config.openid_signing_key = Some(configured_key); diff --git a/crates/defguard_core/tests/integration/api/openid.rs b/crates/defguard_core/tests/integration/api/openid.rs index 27014b0ad..a10a21ae3 100644 --- a/crates/defguard_core/tests/integration/api/openid.rs +++ b/crates/defguard_core/tests/integration/api/openid.rs @@ -4,8 +4,9 @@ use axum::http::header::ToStrError; use defguard_common::db::{ Id, models::{ - OAuth2AuthorizedApp, Settings, User, oauth2client::OAuth2Client, - settings::update_current_settings, + OAuth2AuthorizedApp, Settings, User, + oauth2client::OAuth2Client, + settings::{OPENID_KEY_SIZE, update_current_settings}, }, }; use defguard_core::handlers::{Auth, openid_clients::NewOpenIDClient}; @@ -37,7 +38,7 @@ use crate::api::PaginatedApiResponse; async fn seed_openid_signing_key(pool: &sqlx::PgPool) { let mut settings = Settings::get_current_settings(); - let key = RsaPrivateKey::new(&mut rand::thread_rng(), 2048).unwrap(); + let key = RsaPrivateKey::new(&mut rand::thread_rng(), OPENID_KEY_SIZE).unwrap(); settings.openid_signing_key_der = Some(key.to_pkcs8_der().unwrap().as_bytes().to_vec()); update_current_settings(pool, settings).await.unwrap(); }