From 826450ccc096fc80d44270c5d45aeaeee2cf8735 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Mon, 14 Jul 2025 22:32:18 +0200
Subject: [PATCH 01/26] update dependencies

---
 Cargo.lock | 8 ++++----
 flake.lock | 6 +++---
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 54b5ef2a41..dc30ecc2e2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -901,9 +901,9 @@ checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0"
 
 [[package]]
 name = "crc32fast"
-version = "1.4.2"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
 dependencies = [
  "cfg-if",
 ]
@@ -6277,9 +6277,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
 
 [[package]]
 name = "winnow"
-version = "0.7.11"
+version = "0.7.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
+checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
 dependencies = [
  "memchr",
 ]
diff --git a/flake.lock b/flake.lock
index 06d0fd1c6c..a874e42a18 100644
--- a/flake.lock
+++ b/flake.lock
@@ -48,11 +48,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1752201818,
-        "narHash": "sha256-d8KczaVT8WFEZdWg//tMAbv8EDyn2YTWcJvSY8gqKBU=",
+        "lastModified": 1752461263,
+        "narHash": "sha256-f4XVgqkWF1vSzPbOG5xvi4aAd/n1GwSNsji3mLMFwYQ=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "bd8f8329780b348fedcd37b53dbbee48c08c496d",
+        "rev": "9cc51d100d24fb7ea13a0bee1480ee84fa12a0ad",
         "type": "github"
       },
       "original": {

From c51363e7313a472eca0bbc0360ce14883718ab5a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Mon, 14 Jul 2025 23:01:03 +0200
Subject: [PATCH 02/26] add new location MFA config to DB model

---
 .../defguard_core/src/db/models/wireguard.rs  | 15 +++++++++---
 ...4203243_add_location_mfa_settings.down.sql | 16 +++++++++++++
 ...714203243_add_location_mfa_settings.up.sql | 23 +++++++++++++++++++
 3 files changed, 51 insertions(+), 3 deletions(-)
 create mode 100644 migrations/20250714203243_add_location_mfa_settings.down.sql
 create mode 100644 migrations/20250714203243_add_location_mfa_settings.up.sql

diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs
index 73b5338d1c..edc061087e 100644
--- a/crates/defguard_core/src/db/models/wireguard.rs
+++ b/crates/defguard_core/src/db/models/wireguard.rs
@@ -11,8 +11,8 @@ use ipnetwork::{IpNetwork, IpNetworkError, NetworkSize};
 use model_derive::Model;
 use rand_core::OsRng;
 use sqlx::{
-    Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, postgres::types::PgInterval,
-    query_as, query_scalar,
+    Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, Type,
+    postgres::types::PgInterval, query_as, query_scalar,
 };
 use thiserror::Error;
 use tokio::sync::broadcast::Sender;
@@ -83,6 +83,14 @@ pub enum GatewayEvent {
     FirewallDisabled(Id),
 }
 
+#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
+#[sqlx(type_name = "location_mfa_type", rename_all = "snake_case")]
+pub enum LocationMfaType {
+    Disabled,
+    Internal,
+    External,
+}
+
 /// Stores configuration required to setup a WireGuard network
 #[derive(Clone, Debug, Deserialize, Eq, Hash, Model, PartialEq, Serialize, ToSchema)]
 #[table(wireguard_network)]
@@ -102,11 +110,12 @@ pub struct WireguardNetwork<I = NoId> {
     #[schema(value_type = String)]
     pub allowed_ips: Vec<IpNetwork>,
     pub connected_at: Option<NaiveDateTime>,
-    pub mfa_enabled: bool,
     pub acl_enabled: bool,
     pub acl_default_allow: bool,
     pub keepalive_interval: i32,
     pub peer_disconnect_threshold: i32,
+    #[model(enum)]
+    pub location_mfa: LocationMfaType,
 }
 
 pub struct WireguardKey {
diff --git a/migrations/20250714203243_add_location_mfa_settings.down.sql b/migrations/20250714203243_add_location_mfa_settings.down.sql
new file mode 100644
index 0000000000..4d313c31e7
--- /dev/null
+++ b/migrations/20250714203243_add_location_mfa_settings.down.sql
@@ -0,0 +1,16 @@
+-- restore boolean `mfa_enabled` column
+ALTER TABLE wireguard_network ADD COLUMN "mfa_enabled" BOOLEAN DEFAULT false;
+
+-- populate based on MFA type
+UPDATE wireguard_network
+SET mfa_enabled = CASE
+    WHEN location_mfa = 'disabled'::location_mfa_type THEN false
+    ELSE true
+END;
+--
+-- make restored column NOT NULL
+ALTER TABLE wireguard_network ALTER COLUMN "mfa_enabled" SET NOT NULL;
+
+-- drop new column and type
+ALTER TABLE wireguard_network DROP COLUMN "location_mfa";
+DROP TYPE location_mfa_type;
diff --git a/migrations/20250714203243_add_location_mfa_settings.up.sql b/migrations/20250714203243_add_location_mfa_settings.up.sql
new file mode 100644
index 0000000000..2239794f2e
--- /dev/null
+++ b/migrations/20250714203243_add_location_mfa_settings.up.sql
@@ -0,0 +1,23 @@
+-- add enum representing location MFA configuration
+CREATE TYPE location_mfa_type AS ENUM (
+    'disabled',
+    'internal',
+    'external'
+);
+
+-- add nullable column to `wireguard_network` table
+ALTER TABLE wireguard_network ADD COLUMN "location_mfa" location_mfa_type DEFAULT 'disabled';
+
+-- populate new column based on value in `mfa_enabled` column
+-- previously only internal MFA was available
+UPDATE wireguard_network
+SET location_mfa = CASE
+    WHEN mfa_enabled = true THEN 'internal'::location_mfa_type
+    ELSE 'disabled'::location_mfa_type
+END;
+
+-- make new column NOT NULL
+ALTER TABLE wireguard_network ALTER COLUMN "location_mfa" SET NOT NULL;
+
+-- drop the `mfa_enabled` column since it's no longer needed
+ALTER TABLE wireguard_network DROP COLUMN mfa_enabled;

From 7d1628ccc367c3682daec452d0e9998022c388de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Tue, 15 Jul 2025 10:12:49 +0200
Subject: [PATCH 03/26] update location struct to include new mfa field

---
 crates/defguard_core/src/db/models/device.rs  | 17 ++++++------
 .../defguard_core/src/db/models/wireguard.rs  | 27 ++++++++++++-------
 .../src/enterprise/db/models/acl.rs           |  9 ++++---
 .../src/enterprise/db/models/acl/tests.rs     |  4 +--
 .../src/enterprise/directory_sync/mod.rs      |  7 +++--
 crates/defguard_core/src/grpc/gateway/mod.rs  |  6 ++---
 crates/defguard_core/src/grpc/utils.rs        |  6 +++--
 .../defguard_core/src/handlers/wireguard.rs   |  4 +--
 crates/defguard_core/src/lib.rs               |  8 +++---
 crates/defguard_core/src/wg_config.rs         |  5 ++--
 .../src/wireguard_peer_disconnect.rs          |  8 +++---
 crates/defguard_core/tests/integration/acl.rs |  8 +++---
 .../integration/wireguard_network_import.rs   |  6 +++--
 13 files changed, 69 insertions(+), 46 deletions(-)

diff --git a/crates/defguard_core/src/db/models/device.rs b/crates/defguard_core/src/db/models/device.rs
index 51dfe2baca..a46ab9a216 100644
--- a/crates/defguard_core/src/db/models/device.rs
+++ b/crates/defguard_core/src/db/models/device.rs
@@ -21,7 +21,7 @@ use utoipa::ToSchema;
 
 use super::{
     error::ModelError,
-    wireguard::{NetworkAddressError, WIREGUARD_MAX_HANDSHAKE, WireguardNetwork},
+    wireguard::{LocationMfaType, NetworkAddressError, WIREGUARD_MAX_HANDSHAKE, WireguardNetwork},
 };
 use crate::{
     AsCsv, KEY_LENGTH,
@@ -499,8 +499,8 @@ impl WireguardNetworkDevice {
         query_as!(
             WireguardNetwork,
             "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
-            connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, \
-            acl_enabled, acl_default_allow \
+            connected_at, keepalive_interval, peer_disconnect_threshold, \
+            acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
             FROM wireguard_network WHERE id = $1",
             self.wireguard_network_id
         )
@@ -692,7 +692,7 @@ impl Device<Id> {
             allowed_ips: network.allowed_ips.clone(),
             pubkey: network.pubkey.clone(),
             dns: network.dns.clone(),
-            mfa_enabled: network.mfa_enabled,
+            mfa_enabled: network.mfa_enabled(),
             keepalive_interval: network.keepalive_interval,
         };
 
@@ -725,7 +725,7 @@ impl Device<Id> {
             allowed_ips: network.allowed_ips.clone(),
             pubkey: network.pubkey.clone(),
             dns: network.dns.clone(),
-            mfa_enabled: network.mfa_enabled,
+            mfa_enabled: network.mfa_enabled(),
             keepalive_interval: network.keepalive_interval,
         };
 
@@ -778,6 +778,7 @@ impl Device<Id> {
                 network_info.push(device_network_info);
 
                 let config = Self::create_config(&network, &wireguard_network_device);
+                let mfa_enabled = network.mfa_enabled();
                 configs.push(DeviceConfig {
                     network_id: network.id,
                     network_name: network.name,
@@ -787,7 +788,7 @@ impl Device<Id> {
                     allowed_ips: network.allowed_ips,
                     pubkey: network.pubkey,
                     dns: network.dns,
-                    mfa_enabled: network.mfa_enabled,
+                    mfa_enabled,
                     keepalive_interval: network.keepalive_interval,
                 });
             }
@@ -934,8 +935,8 @@ impl Device<Id> {
         query_as!(
             WireguardNetwork,
             "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
-            connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, \
-            acl_enabled, acl_default_allow \
+            connected_at,  keepalive_interval, peer_disconnect_threshold, \
+            acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
             FROM wireguard_network WHERE id IN \
             (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)",
             self.id
diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs
index edc061087e..71d8cf11cd 100644
--- a/crates/defguard_core/src/db/models/wireguard.rs
+++ b/crates/defguard_core/src/db/models/wireguard.rs
@@ -83,9 +83,10 @@ pub enum GatewayEvent {
     FirewallDisabled(Id),
 }
 
-#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
+#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, ToSchema, Type)]
 #[sqlx(type_name = "location_mfa_type", rename_all = "snake_case")]
 pub enum LocationMfaType {
+    #[default]
     Disabled,
     Internal,
     External,
@@ -149,11 +150,11 @@ impl Default for WireguardNetwork<Id> {
             dns: Option::default(),
             allowed_ips: Vec::default(),
             connected_at: Option::default(),
-            mfa_enabled: false,
             keepalive_interval: DEFAULT_KEEPALIVE_INTERVAL,
             peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD,
             acl_default_allow: false,
             acl_enabled: false,
+            location_mfa: LocationMfaType::default(),
         }
     }
 }
@@ -206,11 +207,11 @@ impl WireguardNetwork {
         endpoint: String,
         dns: Option<String>,
         allowed_ips: Vec<IpNetwork>,
-        mfa_enabled: bool,
         keepalive_interval: i32,
         peer_disconnect_threshold: i32,
         acl_enabled: bool,
         acl_default_allow: bool,
+        location_mfa: LocationMfaType,
     ) -> Result<Self, WireguardNetworkError> {
         let prvkey = StaticSecret::random_from_rng(OsRng);
         let pubkey = PublicKey::from(&prvkey);
@@ -225,11 +226,12 @@ impl WireguardNetwork {
             dns,
             allowed_ips,
             connected_at: None,
-            mfa_enabled,
+
             keepalive_interval,
             peer_disconnect_threshold,
             acl_enabled,
             acl_default_allow,
+            location_mfa,
         })
     }
 
@@ -259,8 +261,8 @@ impl WireguardNetwork<Id> {
         let networks = query_as!(
             WireguardNetwork,
             "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
-            connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, \
-            acl_enabled, acl_default_allow \
+            connected_at, keepalive_interval, peer_disconnect_threshold, \
+            acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
             FROM wireguard_network WHERE name = $1",
             name
         )
@@ -1236,6 +1238,13 @@ impl WireguardNetwork<Id> {
 
         Ok(())
     }
+
+    pub fn mfa_enabled(&self) -> bool {
+        match self.location_mfa {
+            LocationMfaType::Internal | LocationMfaType::External => true,
+            LocationMfaType::Disabled => false,
+        }
+    }
 }
 
 // [`IpNetwork`] does not implement [`Default`]
@@ -1252,11 +1261,11 @@ impl Default for WireguardNetwork {
             dns: Option::default(),
             allowed_ips: Vec::default(),
             connected_at: Option::default(),
-            mfa_enabled: false,
             keepalive_interval: DEFAULT_KEEPALIVE_INTERVAL,
             peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD,
             acl_enabled: false,
             acl_default_allow: false,
+            location_mfa: LocationMfaType::default(),
         }
     }
 }
@@ -1964,11 +1973,11 @@ mod test {
             String::new(),
             None,
             vec![IpNetwork::from_str("10.1.1.0/24").unwrap()],
-            false,
             300,
             300,
             false,
             false,
+            LocationMfaType::Disabled,
         )
         .unwrap()
         .save(&pool)
@@ -2096,11 +2105,11 @@ mod test {
             String::new(),
             None,
             vec![IpNetwork::from_str("10.1.1.0/24").unwrap()],
-            false,
             300,
             300,
             false,
             false,
+            LocationMfaType::Disabled,
         )
         .unwrap()
         .save(&pool)
diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs
index fe4d9567f0..24817a87e1 100644
--- a/crates/defguard_core/src/enterprise/db/models/acl.rs
+++ b/crates/defguard_core/src/enterprise/db/models/acl.rs
@@ -17,7 +17,10 @@ use thiserror::Error;
 use crate::{
     DeviceType,
     appstate::AppState,
-    db::{Device, GatewayEvent, Group, Id, NoId, User, WireguardNetwork},
+    db::{
+        Device, GatewayEvent, Group, Id, NoId, User, WireguardNetwork,
+        models::wireguard::LocationMfaType,
+    },
     enterprise::{
         firewall::FirewallError,
         handlers::acl::{ApiAclAlias, ApiAclRule, EditAclAlias, EditAclRule},
@@ -904,8 +907,8 @@ impl AclRule<Id> {
             query_as!(
                 WireguardNetwork,
                 "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
-                connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, \
-                acl_enabled, acl_default_allow \
+                connected_at, keepalive_interval, peer_disconnect_threshold, \
+                acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
                 FROM aclrulenetwork r \
                 JOIN wireguard_network n \
                 ON n.id = r.network_id \
diff --git a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs
index bf30fbdfe8..391f0f97fd 100644
--- a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs
+++ b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs
@@ -177,11 +177,11 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) {
         "endpoint1".to_string(),
         None,
         Vec::new(),
-        false,
         100,
         100,
         false,
         false,
+        LocationMfaType::Disabled,
     )
     .unwrap()
     .save(&pool)
@@ -194,11 +194,11 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) {
         "endpoint2".to_string(),
         None,
         Vec::new(),
-        false,
         200,
         200,
         false,
         false,
+        LocationMfaType::Disabled,
     )
     .unwrap()
     .save(&pool)
diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs
index 2fbb6018b9..8b6df782fc 100644
--- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs
+++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs
@@ -911,7 +911,10 @@ mod test {
         config::DefGuardConfig,
         db::{
             Device, Session, SessionState, Settings, WireguardNetwork,
-            models::{device::DeviceType, settings::initialize_current_settings},
+            models::{
+                device::DeviceType, settings::initialize_current_settings,
+                wireguard::LocationMfaType,
+            },
             setup_pool,
         },
         enterprise::db::models::openid_provider::DirectorySyncTarget,
@@ -948,11 +951,11 @@ mod test {
             "123.123.123.123".to_string(),
             None,
             vec![],
-            false,
             32,
             32,
             false,
             false,
+            LocationMfaType::Disabled,
         )
         .unwrap()
         .save(pool)
diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs
index ee0870a804..dd5293e575 100644
--- a/crates/defguard_core/src/grpc/gateway/mod.rs
+++ b/crates/defguard_core/src/grpc/gateway/mod.rs
@@ -106,7 +106,7 @@ impl WireguardNetwork<Id> {
             AND u.is_active = true \
             ORDER BY d.id ASC",
             self.id,
-            self.mfa_enabled
+            self.mfa_enabled()
         )
         .fetch_all(executor)
         .await?;
@@ -393,7 +393,7 @@ impl GatewayUpdatesHandler {
                         .find(|info| info.network_id == self.network_id)
                     {
                         Some(network_info) => {
-                            if self.network.mfa_enabled && !network_info.is_authorized {
+                            if self.network.mfa_enabled() && !network_info.is_authorized {
                                 debug!(
                                     "Created WireGuard device {} is not authorized to connect to MFA enabled location {}",
                                     device.device.name, self.network.name
@@ -428,7 +428,7 @@ impl GatewayUpdatesHandler {
                         .find(|info| info.network_id == self.network_id)
                     {
                         Some(network_info) => {
-                            if self.network.mfa_enabled && !network_info.is_authorized {
+                            if self.network.mfa_enabled() && !network_info.is_authorized {
                                 debug!(
                                     "Modified WireGuard device {} is not authorized to connect to MFA enabled location {}",
                                     device.device.name, self.network.name
diff --git a/crates/defguard_core/src/grpc/utils.rs b/crates/defguard_core/src/grpc/utils.rs
index 15ed467947..e2d3191945 100644
--- a/crates/defguard_core/src/grpc/utils.rs
+++ b/crates/defguard_core/src/grpc/utils.rs
@@ -118,6 +118,7 @@ pub(crate) async fn build_device_config_response(
                     );
                     Status::internal(format!("unexpected error: {err}"))
                 })?;
+            let mfa_enabled = network.mfa_enabled();
             let config = ProtoDeviceConfig {
                 config: Device::create_config(&network, &wireguard_network_device),
                 network_id: network.id,
@@ -127,7 +128,7 @@ pub(crate) async fn build_device_config_response(
                 pubkey: network.pubkey,
                 allowed_ips: network.allowed_ips.as_csv(),
                 dns: network.dns,
-                mfa_enabled: network.mfa_enabled,
+                mfa_enabled,
                 keepalive_interval: network.keepalive_interval,
             };
             configs.push(config);
@@ -145,6 +146,7 @@ pub(crate) async fn build_device_config_response(
                 );
                 Status::internal(format!("unexpected error: {err}"))
             })?;
+            let mfa_enabled = network.mfa_enabled();
             if let Some(wireguard_network_device) = wireguard_network_device {
                 let config = ProtoDeviceConfig {
                     config: Device::create_config(&network, &wireguard_network_device),
@@ -155,7 +157,7 @@ pub(crate) async fn build_device_config_response(
                     pubkey: network.pubkey,
                     allowed_ips: network.allowed_ips.as_csv(),
                     dns: network.dns,
-                    mfa_enabled: network.mfa_enabled,
+                    mfa_enabled,
                     keepalive_interval: network.keepalive_interval,
                 };
                 configs.push(config);
diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs
index d0b32d61fd..2fdd44217e 100644
--- a/crates/defguard_core/src/handlers/wireguard.rs
+++ b/crates/defguard_core/src/handlers/wireguard.rs
@@ -145,11 +145,11 @@ pub(crate) async fn create_network(
         data.endpoint,
         data.dns,
         allowed_ips,
-        data.mfa_enabled,
         data.keepalive_interval,
         data.peer_disconnect_threshold,
         data.acl_enabled,
         data.acl_default_allow,
+        todo!(),
     )
     .map_err(|_| WebError::Serialization("Invalid network address".into()))?;
 
@@ -233,11 +233,11 @@ pub(crate) async fn modify_network(
     network.port = data.port;
     network.dns = data.dns;
     network.address = parse_address_list(&data.address);
-    network.mfa_enabled = data.mfa_enabled;
     network.keepalive_interval = data.keepalive_interval;
     network.peer_disconnect_threshold = data.peer_disconnect_threshold;
     network.acl_enabled = data.acl_enabled;
     network.acl_default_allow = data.acl_default_allow;
+    network.location_mfa = todo!();
 
     network.save(&mut *transaction).await?;
     network
diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs
index 2caf85f3db..dbf540661a 100644
--- a/crates/defguard_core/src/lib.rs
+++ b/crates/defguard_core/src/lib.rs
@@ -13,7 +13,7 @@ use axum::{
     routing::{delete, get, patch, post, put},
     serve,
 };
-use db::models::device::DeviceType;
+use db::models::{device::DeviceType, wireguard::LocationMfaType};
 use defguard_web_ui::{index, svg, web_asset};
 use enterprise::{
     handlers::{
@@ -734,11 +734,11 @@ pub async fn init_dev_env(config: &DefGuardConfig) {
             "0.0.0.0".to_string(),
             None,
             vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 1, 1, 0)), 24).unwrap()],
-            false,
             DEFAULT_KEEPALIVE_INTERVAL,
             DEFAULT_DISCONNECT_THRESHOLD,
             false,
             false,
+            LocationMfaType::Disabled,
         )
         .expect("Could not create network");
         network.pubkey = "zGMeVGm9HV9I4wSKF9AXmYnnAIhDySyqLMuKpcfIaQo=".to_string();
@@ -833,11 +833,11 @@ pub async fn init_vpn_location(
                 args.endpoint.clone(),
                 args.dns.clone(),
                 args.allowed_ips.clone(),
-                false,
                 DEFAULT_KEEPALIVE_INTERVAL,
                 DEFAULT_DISCONNECT_THRESHOLD,
                 false,
                 false,
+                LocationMfaType::Disabled,
             )?
             .save(&mut *transaction)
             .await?;
@@ -872,11 +872,11 @@ pub async fn init_vpn_location(
             args.endpoint.clone(),
             args.dns.clone(),
             args.allowed_ips.clone(),
-            false,
             DEFAULT_KEEPALIVE_INTERVAL,
             DEFAULT_DISCONNECT_THRESHOLD,
             false,
             false,
+            LocationMfaType::Disabled,
         )?
         .save(pool)
         .await?
diff --git a/crates/defguard_core/src/wg_config.rs b/crates/defguard_core/src/wg_config.rs
index 0287b3f741..651d33123e 100644
--- a/crates/defguard_core/src/wg_config.rs
+++ b/crates/defguard_core/src/wg_config.rs
@@ -10,7 +10,8 @@ use crate::{
     db::{
         Device, WireguardNetwork,
         models::wireguard::{
-            DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, WireguardNetworkError,
+            DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaType,
+            WireguardNetworkError,
         },
     },
 };
@@ -106,11 +107,11 @@ pub(crate) fn parse_wireguard_config(
         String::new(),
         dns,
         allowed_ips,
-        false,
         DEFAULT_KEEPALIVE_INTERVAL,
         DEFAULT_DISCONNECT_THRESHOLD,
         false,
         false,
+        LocationMfaType::Disabled,
     )?;
     network.pubkey = pubkey;
     network.prvkey = prvkey.to_string();
diff --git a/crates/defguard_core/src/wireguard_peer_disconnect.rs b/crates/defguard_core/src/wireguard_peer_disconnect.rs
index 20feff6e82..867faa7aea 100644
--- a/crates/defguard_core/src/wireguard_peer_disconnect.rs
+++ b/crates/defguard_core/src/wireguard_peer_disconnect.rs
@@ -27,7 +27,7 @@ use crate::{
         models::{
             device::{DeviceInfo, DeviceNetworkInfo, DeviceType, WireguardNetworkDevice},
             error::ModelError,
-            wireguard::WireguardNetworkError,
+            wireguard::{LocationMfaType, WireguardNetworkError},
         },
     },
     events::{InternalEvent, InternalEventContext},
@@ -96,9 +96,9 @@ pub async fn run_periodic_peer_disconnect(
             WireguardNetwork::<Id>,
             "SELECT \
                 id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
-                connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, \
-                acl_enabled, acl_default_allow \
-            FROM wireguard_network WHERE mfa_enabled = true",
+                connected_at, keepalive_interval, peer_disconnect_threshold, \
+                acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
+            FROM wireguard_network WHERE location_mfa != 'disabled'::location_mfa_type",
         )
         .fetch_all(&pool)
         .await?;
diff --git a/crates/defguard_core/tests/integration/acl.rs b/crates/defguard_core/tests/integration/acl.rs
index 769ed1a951..0823023625 100644
--- a/crates/defguard_core/tests/integration/acl.rs
+++ b/crates/defguard_core/tests/integration/acl.rs
@@ -2,7 +2,9 @@ use defguard_core::{
     config::DefGuardConfig,
     db::{
         Device, Group, Id, User, WireguardNetwork,
-        models::{device::DeviceType, settings::initialize_current_settings},
+        models::{
+            device::DeviceType, settings::initialize_current_settings, wireguard::LocationMfaType,
+        },
     },
     enterprise::{
         db::models::acl::{AclAlias, AclRule, AliasKind, AliasState, RuleState},
@@ -418,11 +420,11 @@ async fn test_related_objects(_: PgPoolOptions, options: PgConnectOptions) {
             "endpoint1".to_string(),
             None,
             Vec::new(),
-            false,
             100,
             100,
             false,
             false,
+            LocationMfaType::Disabled,
         )
         .unwrap()
         .save(&pool)
@@ -759,11 +761,11 @@ async fn test_rule_delete_state_applied(_: PgPoolOptions, options: PgConnectOpti
         "endpoint1".to_string(),
         None,
         Vec::new(),
-        false,
         100,
         100,
         false,
         false,
+        LocationMfaType::Disabled,
     )
     .unwrap()
     .save(&pool)
diff --git a/crates/defguard_core/tests/integration/wireguard_network_import.rs b/crates/defguard_core/tests/integration/wireguard_network_import.rs
index c60512e015..61cd17b52a 100644
--- a/crates/defguard_core/tests/integration/wireguard_network_import.rs
+++ b/crates/defguard_core/tests/integration/wireguard_network_import.rs
@@ -5,7 +5,9 @@ use defguard_core::{
         Device, GatewayEvent, WireguardNetwork,
         models::{
             device::{DeviceType, UserDevice},
-            wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL},
+            wireguard::{
+                DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaType,
+            },
         },
     },
     handlers::{Auth, wireguard::ImportedNetworkData},
@@ -55,11 +57,11 @@ async fn test_config_import(_: PgPoolOptions, options: PgConnectOptions) {
         String::new(),
         None,
         vec![],
-        false,
         DEFAULT_KEEPALIVE_INTERVAL,
         DEFAULT_DISCONNECT_THRESHOLD,
         false,
         false,
+        LocationMfaType::Disabled,
     )
     .unwrap();
     initial_network.save(&pool).await.unwrap();

From 0da6a46e5c46fe40844fb110ae9574de81c00898 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Tue, 15 Jul 2025 10:33:17 +0200
Subject: [PATCH 04/26] update API to expect MFA type

---
 crates/defguard_core/src/handlers/wireguard.rs      | 10 +++++-----
 crates/defguard_core/tests/integration/wireguard.rs |  6 ++++--
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs
index 2fdd44217e..cbb599301b 100644
--- a/crates/defguard_core/src/handlers/wireguard.rs
+++ b/crates/defguard_core/src/handlers/wireguard.rs
@@ -30,8 +30,8 @@ use crate::{
                 WireguardNetworkDevice,
             },
             wireguard::{
-                DateTimeAggregation, MappedDevice, WireguardDeviceStatsRow, WireguardNetworkInfo,
-                WireguardNetworkStats, WireguardUserStatsRow, networks_stats,
+                DateTimeAggregation, LocationMfaType, MappedDevice, WireguardDeviceStatsRow,
+                WireguardNetworkInfo, WireguardNetworkStats, WireguardUserStatsRow, networks_stats,
             },
         },
     },
@@ -75,11 +75,11 @@ pub struct WireguardNetworkData {
     pub allowed_ips: Option<String>,
     pub dns: Option<String>,
     pub allowed_groups: Vec<String>,
-    pub mfa_enabled: bool,
     pub keepalive_interval: i32,
     pub peer_disconnect_threshold: i32,
     pub acl_enabled: bool,
     pub acl_default_allow: bool,
+    pub location_mfa: LocationMfaType,
 }
 
 impl WireguardNetworkData {
@@ -149,7 +149,7 @@ pub(crate) async fn create_network(
         data.peer_disconnect_threshold,
         data.acl_enabled,
         data.acl_default_allow,
-        todo!(),
+        data.location_mfa,
     )
     .map_err(|_| WebError::Serialization("Invalid network address".into()))?;
 
@@ -237,7 +237,7 @@ pub(crate) async fn modify_network(
     network.peer_disconnect_threshold = data.peer_disconnect_threshold;
     network.acl_enabled = data.acl_enabled;
     network.acl_default_allow = data.acl_default_allow;
-    network.location_mfa = todo!();
+    network.location_mfa = data.location_mfa;
 
     network.save(&mut *transaction).await?;
     network
diff --git a/crates/defguard_core/tests/integration/wireguard.rs b/crates/defguard_core/tests/integration/wireguard.rs
index 4d5ed9eb3d..572e3b1bc5 100644
--- a/crates/defguard_core/tests/integration/wireguard.rs
+++ b/crates/defguard_core/tests/integration/wireguard.rs
@@ -5,7 +5,9 @@ use defguard_core::{
         Device, GatewayEvent, Id, WireguardNetwork,
         models::{
             device::WireguardNetworkDevice,
-            wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL},
+            wireguard::{
+                DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaType,
+            },
         },
     },
     handlers::{Auth, GroupInfo, wireguard::WireguardNetworkData},
@@ -56,11 +58,11 @@ async fn test_network(_: PgPoolOptions, options: PgConnectOptions) {
         allowed_ips: Some("10.1.1.0/24, 10.2.0.1/16, 10.10.10.54/32".into()),
         dns: None,
         allowed_groups: vec!["admin".into()],
-        mfa_enabled: false,
         keepalive_interval: DEFAULT_KEEPALIVE_INTERVAL,
         peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD,
         acl_enabled: false,
         acl_default_allow: false,
+        location_mfa: LocationMfaType::Disabled,
     };
     let response = client
         .put(format!("/api/v1/network/{}", network.id))

From 81a78c4e1fa6f3b0f3ccd29572fb18f300e564b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Tue, 15 Jul 2025 11:12:15 +0200
Subject: [PATCH 05/26] fix translation for network device setup

---
 .../AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx
index 290440d35e..ff42069906 100644
--- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx
+++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx
@@ -136,8 +136,8 @@ export const MethodStep = () => {
         />
         <DeviceSetupMethodCard
           custom={{
-            title: localLL.cards.cli.title(),
-            description: localLL.cards.cli.subtitle(),
+            title: localLL.cards.manual.title(),
+            description: localLL.cards.manual.subtitle(),
             icon: <SvgWireguardLogo />,
             testId: 'standalone-device-choice-card-manual',
           }}

From d499bb839c315fb3a1b03fa4c743e16a6fc30f25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Tue, 15 Jul 2025 13:13:33 +0200
Subject: [PATCH 06/26] add temporary frontend for setting location MFA type

---
 crates/defguard_core/src/add_network_device   |  0
 .../defguard_core/src/db/models/wireguard.rs  |  3 +-
 web/src/i18n/en/index.ts                      |  8 ++++
 web/src/i18n/i18n-types.ts                    | 40 ++++++++++++++++
 .../NetworkEditForm/NetworkEditForm.tsx       | 13 ++---
 .../WizardNetworkConfiguration.tsx            | 10 ++--
 web/src/pages/wizard/hooks/useWizardStore.ts  | 10 ++--
 .../FormLocationMfaTypeSelect.tsx             | 48 +++++++++++++++++++
 web/src/shared/types.ts                       |  8 +++-
 9 files changed, 121 insertions(+), 19 deletions(-)
 create mode 100644 crates/defguard_core/src/add_network_device
 create mode 100644 web/src/shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx

diff --git a/crates/defguard_core/src/add_network_device b/crates/defguard_core/src/add_network_device
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs
index 71d8cf11cd..b8fa5c5e1d 100644
--- a/crates/defguard_core/src/db/models/wireguard.rs
+++ b/crates/defguard_core/src/db/models/wireguard.rs
@@ -84,7 +84,8 @@ pub enum GatewayEvent {
 }
 
 #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, ToSchema, Type)]
-#[sqlx(type_name = "location_mfa_type", rename_all = "snake_case")]
+#[sqlx(type_name = "location_mfa_type", rename_all = "lowercase")]
+#[serde(rename_all = "lowercase")]
 pub enum LocationMfaType {
     #[default]
     Disabled,
diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts
index 9542af8d46..01a7edbd8d 100644
--- a/web/src/i18n/en/index.ts
+++ b/web/src/i18n/en/index.ts
@@ -1130,6 +1130,14 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
         contact: 'by contacting:',
       },
     },
+    locationMfaTypeSelect: {
+      label: 'MFA Requirement',
+      options: {
+        disabled: 'Do not enforce MFA',
+        internal: 'Internal MFA',
+        external: 'External MFA',
+      },
+    },
   },
   settingsPage: {
     title: 'Settings',
diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts
index d76ffc704f..ea5d392b94 100644
--- a/web/src/i18n/i18n-types.ts
+++ b/web/src/i18n/i18n-types.ts
@@ -2779,6 +2779,26 @@ type RootTranslation = {
 				contact: string
 			}
 		}
+		locationMfaTypeSelect: {
+			/**
+			 * M​F​A​ ​R​e​q​u​i​r​e​m​e​n​t
+			 */
+			label: string
+			options: {
+				/**
+				 * D​o​ ​n​o​t​ ​e​n​f​o​r​c​e​ ​M​F​A
+				 */
+				disabled: string
+				/**
+				 * I​n​t​e​r​n​a​l​ ​M​F​A
+				 */
+				internal: string
+				/**
+				 * E​x​t​e​r​n​a​l​ ​M​F​A
+				 */
+				external: string
+			}
+		}
 	}
 	settingsPage: {
 		/**
@@ -9345,6 +9365,26 @@ export type TranslationFunctions = {
 				contact: () => LocalizedString
 			}
 		}
+		locationMfaTypeSelect: {
+			/**
+			 * MFA Requirement
+			 */
+			label: () => LocalizedString
+			options: {
+				/**
+				 * Do not enforce MFA
+				 */
+				disabled: () => LocalizedString
+				/**
+				 * Internal MFA
+				 */
+				internal: () => LocalizedString
+				/**
+				 * External MFA
+				 */
+				external: () => LocalizedString
+			}
+		}
 	}
 	settingsPage: {
 		/**
diff --git a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
index 9ddd1ad7d4..7b268ec9e3 100644
--- a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
+++ b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
@@ -11,6 +11,7 @@ import { shallow } from 'zustand/shallow';
 
 import { useI18nContext } from '../../../i18n/i18n-react';
 import { FormAclDefaultPolicy } from '../../../shared/components/Form/FormAclDefaultPolicySelect/FormAclDefaultPolicy.tsx';
+import { FormLocationMfaTypeSelect } from '../../../shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx';
 import { FormCheckBox } from '../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx';
 import { FormInput } from '../../../shared/defguard-ui/components/Form/FormInput/FormInput';
 import { FormSelect } from '../../../shared/defguard-ui/components/Form/FormSelect/FormSelect';
@@ -21,7 +22,7 @@ import { useAppStore } from '../../../shared/hooks/store/useAppStore.ts';
 import useApi from '../../../shared/hooks/useApi';
 import { useToaster } from '../../../shared/hooks/useToaster';
 import { QueryKeys } from '../../../shared/queries';
-import type { Network } from '../../../shared/types';
+import { LocationMfaType, type Network } from '../../../shared/types';
 import { titleCase } from '../../../shared/utils/titleCase';
 import { trimObjectStrings } from '../../../shared/utils/trimObjectStrings.ts';
 import {
@@ -141,7 +142,6 @@ export const NetworkEditForm = () => {
             return validateIpOrDomainList(val, ',', false, true);
           }, LL.form.error.allowedIps()),
         allowed_groups: z.array(z.string().min(1, LL.form.error.minimumLength())),
-        mfa_enabled: z.boolean(),
         keepalive_interval: z
           .number({
             invalid_type_error: LL.form.error.required(),
@@ -155,6 +155,7 @@ export const NetworkEditForm = () => {
           .min(120, LL.form.error.invalid()),
         acl_enabled: z.boolean(),
         acl_default_allow: z.boolean(),
+        location_mfa: z.nativeEnum(LocationMfaType),
       }),
     [LL.form.error],
   );
@@ -170,11 +171,11 @@ export const NetworkEditForm = () => {
       allowed_ips: '',
       allowed_groups: [],
       dns: '',
-      mfa_enabled: false,
       keepalive_interval: 25,
       peer_disconnect_threshold: 180,
       acl_enabled: false,
       acl_default_allow: false,
+      location_mfa: LocationMfaType.DISABLED,
     }),
     [],
   );
@@ -315,11 +316,6 @@ export const NetworkEditForm = () => {
             displayValue: titleCase(val),
           })}
         />
-        <FormCheckBox
-          controller={{ control, name: 'mfa_enabled' }}
-          label={LL.networkConfiguration.form.fields.mfa_enabled.label()}
-          labelPlacement="right"
-        />
         {!enterpriseEnabled && (
           <MessageBox type={MessageBoxType.WARNING}>
             <p>{LL.networkConfiguration.form.helpers.aclFeatureDisabled()}</p>
@@ -345,6 +341,7 @@ export const NetworkEditForm = () => {
           label={LL.networkConfiguration.form.fields.peer_disconnect_threshold.label()}
           type="number"
         />
+        <FormLocationMfaTypeSelect controller={{ control, name: 'location_mfa' }} />
         <button type="submit" className="hidden" ref={submitRef}></button>
       </form>
     </section>
diff --git a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx
index d032ff56f0..05fa368f81 100644
--- a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx
+++ b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx
@@ -9,6 +9,7 @@ import { shallow } from 'zustand/shallow';
 
 import { useI18nContext } from '../../../../i18n/i18n-react';
 import { FormAclDefaultPolicy } from '../../../../shared/components/Form/FormAclDefaultPolicySelect/FormAclDefaultPolicy.tsx';
+import { FormLocationMfaTypeSelect } from '../../../../shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx';
 import { FormCheckBox } from '../../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx';
 import { FormInput } from '../../../../shared/defguard-ui/components/Form/FormInput/FormInput';
 import { FormSelect } from '../../../../shared/defguard-ui/components/Form/FormSelect/FormSelect';
@@ -18,6 +19,7 @@ import type { SelectOption } from '../../../../shared/defguard-ui/components/Lay
 import useApi from '../../../../shared/hooks/useApi';
 import { useToaster } from '../../../../shared/hooks/useToaster';
 import { QueryKeys } from '../../../../shared/queries';
+import { LocationMfaType } from '../../../../shared/types.ts';
 import { titleCase } from '../../../../shared/utils/titleCase';
 import { trimObjectStrings } from '../../../../shared/utils/trimObjectStrings.ts';
 import { validateIpList, validateIpOrDomainList } from '../../../../shared/validators';
@@ -118,7 +120,6 @@ export const WizardNetworkConfiguration = () => {
             return validateIpOrDomainList(val, ',', true);
           }, LL.form.error.allowedIps()),
         allowed_groups: z.array(z.string().min(1, LL.form.error.minimumLength())),
-        mfa_enabled: z.boolean(),
         keepalive_interval: z
           .number({
             invalid_type_error: LL.form.error.invalid(),
@@ -131,6 +132,7 @@ export const WizardNetworkConfiguration = () => {
           .refine((v) => v >= 120, LL.form.error.minimumLength()),
         acl_enabled: z.boolean(),
         acl_default_allow: z.boolean(),
+        location_mfa: z.nativeEnum(LocationMfaType),
       }),
     [LL.form.error],
   );
@@ -221,11 +223,6 @@ export const WizardNetworkConfiguration = () => {
             displayValue: titleCase(group),
           })}
         />
-        <FormCheckBox
-          controller={{ control, name: 'mfa_enabled' }}
-          label={LL.networkConfiguration.form.fields.mfa_enabled.label()}
-          labelPlacement="right"
-        />
         <FormCheckBox
           controller={{ control, name: 'acl_enabled' }}
           label={LL.networkConfiguration.form.fields.acl_enabled.label()}
@@ -242,6 +239,7 @@ export const WizardNetworkConfiguration = () => {
           label={LL.networkConfiguration.form.fields.peer_disconnect_threshold.label()}
           type="number"
         />
+        <FormLocationMfaTypeSelect controller={{ control, name: 'location_mfa' }} />
         <input type="submit" className="visually-hidden" ref={submitRef} />
       </form>
     </Card>
diff --git a/web/src/pages/wizard/hooks/useWizardStore.ts b/web/src/pages/wizard/hooks/useWizardStore.ts
index 0f9e258481..6102d8cee0 100644
--- a/web/src/pages/wizard/hooks/useWizardStore.ts
+++ b/web/src/pages/wizard/hooks/useWizardStore.ts
@@ -3,7 +3,11 @@ import { Subject } from 'rxjs';
 import { createJSONStorage, persist } from 'zustand/middleware';
 import { createWithEqualityFn } from 'zustand/traditional';
 
-import type { ImportedDevice, Network } from '../../../shared/types';
+import {
+  type ImportedDevice,
+  LocationMfaType,
+  type Network,
+} from '../../../shared/types';
 
 export enum WizardSetupType {
   IMPORT = 'IMPORT',
@@ -25,11 +29,11 @@ const defaultValues: StoreFields = {
     allowed_ips: '',
     allowed_groups: [],
     dns: '',
-    mfa_enabled: false,
     keepalive_interval: 25,
     peer_disconnect_threshold: 180,
     acl_enabled: false,
     acl_default_allow: false,
+    location_mfa: LocationMfaType.DISABLED,
   },
 };
 
@@ -81,11 +85,11 @@ type StoreFields = {
     allowed_ips: string;
     allowed_groups: string[];
     dns?: string;
-    mfa_enabled: boolean;
     keepalive_interval: number;
     peer_disconnect_threshold: number;
     acl_enabled: boolean;
     acl_default_allow: boolean;
+    location_mfa: LocationMfaType;
   };
 };
 
diff --git a/web/src/shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx b/web/src/shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx
new file mode 100644
index 0000000000..aed2c0910b
--- /dev/null
+++ b/web/src/shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx
@@ -0,0 +1,48 @@
+import { useMemo } from 'react';
+import type { FieldValues, UseControllerProps } from 'react-hook-form';
+
+import { useI18nContext } from '../../../../i18n/i18n-react';
+import { FormSelect } from '../../../defguard-ui/components/Form/FormSelect/FormSelect';
+import type { SelectOption } from '../../../defguard-ui/components/Layout/Select/types';
+import { LocationMfaType } from '../../../types';
+
+type Props<T extends FieldValues> = {
+  controller: UseControllerProps<T>;
+  disabled?: boolean;
+};
+
+export const FormLocationMfaTypeSelect = <T extends FieldValues>({
+  controller,
+  disabled = false,
+}: Props<T>) => {
+  const { LL } = useI18nContext();
+
+  const options = useMemo(
+    (): SelectOption<LocationMfaType>[] => [
+      {
+        key: LocationMfaType.DISABLED,
+        value: LocationMfaType.DISABLED,
+        label: LL.components.locationMfaTypeSelect.options.disabled(),
+      },
+      {
+        key: LocationMfaType.INTERNAL,
+        value: LocationMfaType.INTERNAL,
+        label: LL.components.locationMfaTypeSelect.options.internal(),
+      },
+      {
+        key: LocationMfaType.EXTERNAL,
+        value: LocationMfaType.EXTERNAL,
+        label: LL.components.locationMfaTypeSelect.options.external(),
+      },
+    ],
+    [LL.components.aclDefaultPolicySelect.options],
+  );
+  return (
+    <FormSelect
+      controller={controller}
+      options={options}
+      label={LL.components.locationMfaTypeSelect.label()}
+      disabled={disabled}
+    />
+  );
+};
diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts
index e64b584cd5..50e609f85b 100644
--- a/web/src/shared/types.ts
+++ b/web/src/shared/types.ts
@@ -118,6 +118,12 @@ export type GatewayStatus = {
   uid: string;
 };
 
+export enum LocationMfaType {
+  DISABLED = 'disabled',
+  INTERNAL = 'internal',
+  EXTERNAL = 'external',
+}
+
 export interface Network {
   id: number;
   name: string;
@@ -130,11 +136,11 @@ export interface Network {
   allowed_ips?: string[];
   allowed_groups?: string[];
   dns?: string;
-  mfa_enabled: boolean;
   keepalive_interval: number;
   peer_disconnect_threshold: number;
   acl_enabled: boolean;
   acl_default_allow: boolean;
+  location_mfa: LocationMfaType;
 }
 
 export type ModifyNetworkRequest = {

From 7510f224f1a623f71cdcfa04208a03e2e5e37a95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Tue, 15 Jul 2025 14:22:38 +0200
Subject: [PATCH 07/26] update protos

---
 proto | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proto b/proto
index c0aef68395..9fc4e466ad 160000
--- a/proto
+++ b/proto
@@ -1 +1 @@
-Subproject commit c0aef68395720f46a7f038b6766de3bb30e02930
+Subproject commit 9fc4e466adae627ae332560c09df871c703b91a3

From a076062cfcc03848d5b87ebadda8151b15451809 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Wed, 16 Jul 2025 09:41:17 +0200
Subject: [PATCH 08/26] handle updated protos in code

---
 .../src/db/models/activity_log/metadata.rs    |  2 --
 crates/defguard_core/src/db/models/device.rs  |  9 ++++----
 .../defguard_core/src/db/models/settings.rs   |  7 ++-----
 .../defguard_core/src/db/models/wireguard.rs  | 21 ++++++++++++++++++-
 .../enterprise/handlers/openid_providers.rs   |  9 +++-----
 .../src/grpc/desktop_client_mfa.rs            | 12 +++--------
 crates/defguard_core/src/grpc/enrollment.rs   | 10 +++++++--
 crates/defguard_core/src/grpc/mod.rs          |  7 -------
 crates/defguard_core/src/grpc/utils.rs        | 14 ++++++++-----
 ...4203243_add_location_mfa_settings.down.sql |  3 +++
 ...714203243_add_location_mfa_settings.up.sql |  3 +++
 11 files changed, 55 insertions(+), 42 deletions(-)

diff --git a/crates/defguard_core/src/db/models/activity_log/metadata.rs b/crates/defguard_core/src/db/models/activity_log/metadata.rs
index 72860e3594..c1c1032cce 100644
--- a/crates/defguard_core/src/db/models/activity_log/metadata.rs
+++ b/crates/defguard_core/src/db/models/activity_log/metadata.rs
@@ -391,7 +391,6 @@ pub struct SettingsNoSecrets {
     // Whether to create a new account when users try to log in with external OpenID
     pub openid_create_account: bool,
     pub openid_username_handling: OpenidUsernameHandling,
-    pub use_openid_for_mfa: bool,
     pub license: Option<String>,
     // Gateway disconnect notifications
     pub gateway_disconnect_notifications_enabled: bool,
@@ -443,7 +442,6 @@ impl From<Settings> for SettingsNoSecrets {
             ldap_sync_groups: value.ldap_sync_groups,
             openid_create_account: value.openid_create_account,
             openid_username_handling: value.openid_username_handling,
-            use_openid_for_mfa: value.use_openid_for_mfa,
             license: value.license,
             gateway_disconnect_notifications_enabled: value
                 .gateway_disconnect_notifications_enabled,
diff --git a/crates/defguard_core/src/db/models/device.rs b/crates/defguard_core/src/db/models/device.rs
index a46ab9a216..07d18e2348 100644
--- a/crates/defguard_core/src/db/models/device.rs
+++ b/crates/defguard_core/src/db/models/device.rs
@@ -40,8 +40,8 @@ pub struct DeviceConfig {
     pub(crate) allowed_ips: Vec<IpNetwork>,
     pub(crate) pubkey: String,
     pub(crate) dns: Option<String>,
-    pub(crate) mfa_enabled: bool,
     pub(crate) keepalive_interval: i32,
+    pub(crate) location_mfa: LocationMfaType,
 }
 
 // The type of a device:
@@ -692,8 +692,8 @@ impl Device<Id> {
             allowed_ips: network.allowed_ips.clone(),
             pubkey: network.pubkey.clone(),
             dns: network.dns.clone(),
-            mfa_enabled: network.mfa_enabled(),
             keepalive_interval: network.keepalive_interval,
+            location_mfa: network.location_mfa.clone(),
         };
 
         Ok((device_network_info, device_config))
@@ -725,8 +725,8 @@ impl Device<Id> {
             allowed_ips: network.allowed_ips.clone(),
             pubkey: network.pubkey.clone(),
             dns: network.dns.clone(),
-            mfa_enabled: network.mfa_enabled(),
             keepalive_interval: network.keepalive_interval,
+            location_mfa: network.location_mfa.clone(),
         };
 
         Ok((device_network_info, device_config))
@@ -778,7 +778,6 @@ impl Device<Id> {
                 network_info.push(device_network_info);
 
                 let config = Self::create_config(&network, &wireguard_network_device);
-                let mfa_enabled = network.mfa_enabled();
                 configs.push(DeviceConfig {
                     network_id: network.id,
                     network_name: network.name,
@@ -788,8 +787,8 @@ impl Device<Id> {
                     allowed_ips: network.allowed_ips,
                     pubkey: network.pubkey,
                     dns: network.dns,
-                    mfa_enabled,
                     keepalive_interval: network.keepalive_interval,
+                    location_mfa: network.location_mfa.clone(),
                 });
             }
         }
diff --git a/crates/defguard_core/src/db/models/settings.rs b/crates/defguard_core/src/db/models/settings.rs
index 3f9eda7077..9eb88aa8a5 100644
--- a/crates/defguard_core/src/db/models/settings.rs
+++ b/crates/defguard_core/src/db/models/settings.rs
@@ -120,7 +120,6 @@ pub struct Settings {
     // Whether to create a new account when users try to log in with external OpenID
     pub openid_create_account: bool,
     pub openid_username_handling: OpenidUsernameHandling,
-    pub use_openid_for_mfa: bool,
     pub license: Option<String>,
     // Gateway disconnect notifications
     pub gateway_disconnect_notifications_enabled: bool,
@@ -153,7 +152,7 @@ impl Settings {
             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, \
-            openid_username_handling \"openid_username_handling: OpenidUsernameHandling\", use_openid_for_mfa \
+            openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" \
             FROM \"settings\" WHERE id = 1",
         )
         .fetch_optional(executor)
@@ -225,8 +224,7 @@ impl Settings {
             ldap_uses_ad = $45, \
             ldap_user_rdn_attr = $46, \
             ldap_sync_groups = $47, \
-            openid_username_handling = $48, \
-            use_openid_for_mfa = $49 \
+            openid_username_handling = $48 \
             WHERE id = 1",
             self.openid_enabled,
             self.wireguard_enabled,
@@ -276,7 +274,6 @@ impl Settings {
             self.ldap_user_rdn_attr,
             &self.ldap_sync_groups as &Vec<String>,
             &self.openid_username_handling as &OpenidUsernameHandling,
-            self.use_openid_for_mfa,
         )
         .execute(executor)
         .await?;
diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs
index b8fa5c5e1d..c7854bcfc0 100644
--- a/crates/defguard_core/src/db/models/wireguard.rs
+++ b/crates/defguard_core/src/db/models/wireguard.rs
@@ -35,7 +35,7 @@ use crate::{
     grpc::{
         GatewayState,
         gateway::{Peer, send_multiple_wireguard_events},
-        proto::enterprise::firewall::FirewallConfig,
+        proto::{enterprise::firewall::FirewallConfig, proxy::LocationMfa as ProtoLocationMfa},
     },
     wg_config::ImportedDevice,
 };
@@ -93,6 +93,25 @@ pub enum LocationMfaType {
     External,
 }
 
+impl From<ProtoLocationMfa> for LocationMfaType {
+    fn from(value: ProtoLocationMfa) -> Self {
+        match value {
+            ProtoLocationMfa::Unspecified | ProtoLocationMfa::Disabled => LocationMfaType::Disabled,
+            ProtoLocationMfa::Internal => LocationMfaType::Internal,
+            ProtoLocationMfa::External => LocationMfaType::External,
+        }
+    }
+}
+impl From<LocationMfaType> for ProtoLocationMfa {
+    fn from(value: LocationMfaType) -> Self {
+        match value {
+            LocationMfaType::Disabled => ProtoLocationMfa::Disabled,
+            LocationMfaType::Internal => ProtoLocationMfa::Internal,
+            LocationMfaType::External => ProtoLocationMfa::External,
+        }
+    }
+}
+
 /// Stores configuration required to setup a WireGuard network
 #[derive(Clone, Debug, Deserialize, Eq, Hash, Model, PartialEq, Serialize, ToSchema)]
 #[table(wireguard_network)]
diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
index a0680217ef..3e12d27a13 100644
--- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
+++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
@@ -118,7 +118,6 @@ pub async fn add_openid_provider(
 
     let mut settings = Settings::get_current_settings();
     settings.openid_create_account = provider_data.create_account;
-    settings.use_openid_for_mfa = provider_data.use_openid_for_mfa;
     settings.openid_username_handling = provider_data.username_handling;
     update_current_settings(&appstate.pool, settings).await?;
 
@@ -187,7 +186,7 @@ pub async fn get_current_openid_provider(
             Ok(ApiResponse {
                 json: json!({
                     "provider": json!(provider),
-                    "settings": json!({ "create_account": settings.openid_create_account, "username_handling": settings.openid_username_handling, "use_openid_for_mfa": settings.use_openid_for_mfa }),
+                    "settings": json!({ "create_account": settings.openid_create_account, "username_handling": settings.openid_username_handling }),
                 }),
                 status: StatusCode::OK,
             })
@@ -195,7 +194,7 @@ pub async fn get_current_openid_provider(
         None => Ok(ApiResponse {
             json: json!({
                 "provider": null,
-                "settings": json!({ "create_account": settings.openid_create_account, "username_handling": settings.openid_username_handling, "use_openid_for_mfa": settings.use_openid_for_mfa }),
+                "settings": json!({ "create_account": settings.openid_create_account, "username_handling": settings.openid_username_handling }),
             }),
             status: StatusCode::NO_CONTENT,
         }),
@@ -217,10 +216,8 @@ pub async fn delete_openid_provider(
     let mut transaction = appstate.pool.begin().await?;
     let provider = OpenIdProvider::find_by_name(&mut *transaction, &provider_data.name).await?;
     if let Some(provider) = provider {
-        let mut settings = Settings::get_current_settings();
         provider.clone().delete(&mut *transaction).await?;
-        settings.use_openid_for_mfa = false;
-        update_current_settings(&mut *transaction, settings).await?;
+        // FIXME: update locations using external MFA
         transaction.commit().await?;
         info!(
             "User {} deleted OpenID provider {}",
diff --git a/crates/defguard_core/src/grpc/desktop_client_mfa.rs b/crates/defguard_core/src/grpc/desktop_client_mfa.rs
index 09c94d03ea..3dabde6671 100644
--- a/crates/defguard_core/src/grpc/desktop_client_mfa.rs
+++ b/crates/defguard_core/src/grpc/desktop_client_mfa.rs
@@ -16,7 +16,7 @@ use super::proto::proxy::{
 use crate::{
     auth::{Claims, ClaimsType},
     db::{
-        Device, GatewayEvent, Id, Settings, User, UserInfo, WireguardNetwork,
+        Device, GatewayEvent, Id, User, UserInfo, WireguardNetwork,
         models::device::{DeviceInfo, DeviceNetworkInfo, WireguardNetworkDevice},
     },
     enterprise::{db::models::openid_provider::OpenIdProvider, is_enterprise_enabled},
@@ -166,6 +166,8 @@ impl ClientMfaServer {
             Status::internal("unexpected error")
         })?;
 
+        // FIXME: check which method is enabled for this location
+
         // check if selected method is enabled
         let method = MfaMethod::try_from(request.method).map_err(|err| {
             error!("Invalid MFA method selected ({}): {err}", request.method);
@@ -204,14 +206,6 @@ impl ClientMfaServer {
                     ));
                 }
 
-                let settings = Settings::get_current_settings();
-                if !settings.use_openid_for_mfa {
-                    error!("OIDC MFA method is not enabled in settings");
-                    return Err(Status::invalid_argument(
-                        "selected MFA method not available",
-                    ));
-                }
-
                 if OpenIdProvider::get_current(&self.pool)
                     .await
                     .map_err(|err| {
diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs
index ce4bf7b64f..2dd4862547 100644
--- a/crates/defguard_core/src/grpc/enrollment.rs
+++ b/crates/defguard_core/src/grpc/enrollment.rs
@@ -23,6 +23,7 @@ use crate::{
             device::{DeviceConfig, DeviceInfo, DeviceType},
             enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError},
             polling_token::PollingToken,
+            wireguard::LocationMfaType,
         },
     },
     enterprise::{
@@ -31,7 +32,10 @@ use crate::{
         limits::update_counts,
     },
     events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, EnrollmentEvent},
-    grpc::utils::{build_device_config_response, new_polling_token, parse_client_info},
+    grpc::{
+        proto::proxy::LocationMfa as ProtoLocationMfa,
+        utils::{build_device_config_response, new_polling_token, parse_client_info},
+    },
     handlers::{mail::send_new_device_added_email, user::check_password_strength},
     headers::get_device_info,
     mail::Mail,
@@ -856,8 +860,10 @@ impl From<DeviceConfig> for ProtoDeviceConfig {
             pubkey: config.pubkey,
             allowed_ips: config.allowed_ips.as_csv(),
             dns: config.dns,
-            mfa_enabled: config.mfa_enabled,
             keepalive_interval: config.keepalive_interval,
+            location_mfa: Some(
+                <LocationMfaType as Into<ProtoLocationMfa>>::into(config.location_mfa).into(),
+            ),
         }
     }
 }
diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs
index 56dd570cf3..89e7e0b16c 100644
--- a/crates/defguard_core/src/grpc/mod.rs
+++ b/crates/defguard_core/src/grpc/mod.rs
@@ -948,7 +948,6 @@ pub struct InstanceInfo {
     username: String,
     disable_all_traffic: bool,
     enterprise_enabled: bool,
-    use_openid_for_mfa: bool,
     openid_display_name: Option<String>,
 }
 
@@ -972,11 +971,6 @@ impl InstanceInfo {
             username: username.into(),
             disable_all_traffic: enterprise_settings.disable_all_traffic,
             enterprise_enabled: is_enterprise_enabled(),
-            use_openid_for_mfa: if is_enterprise_enabled() {
-                settings.use_openid_for_mfa
-            } else {
-                false
-            },
             openid_display_name,
         }
     }
@@ -992,7 +986,6 @@ impl From<InstanceInfo> for proto::proxy::InstanceInfo {
             username: instance.username,
             disable_all_traffic: instance.disable_all_traffic,
             enterprise_enabled: instance.enterprise_enabled,
-            use_openid_for_mfa: instance.use_openid_for_mfa,
             openid_display_name: instance.openid_display_name,
         }
     }
diff --git a/crates/defguard_core/src/grpc/utils.rs b/crates/defguard_core/src/grpc/utils.rs
index e2d3191945..e5df767e1d 100644
--- a/crates/defguard_core/src/grpc/utils.rs
+++ b/crates/defguard_core/src/grpc/utils.rs
@@ -14,12 +14,13 @@ use crate::{
         models::{
             device::{DeviceType, WireguardNetworkDevice},
             polling_token::PollingToken,
-            wireguard::WireguardNetwork,
+            wireguard::{LocationMfaType, WireguardNetwork},
         },
     },
     enterprise::db::models::{
         enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider,
     },
+    grpc::proto::proxy::LocationMfa as ProtoLocationMfa,
 };
 
 // Create a new token for configuration polling.
@@ -118,7 +119,6 @@ pub(crate) async fn build_device_config_response(
                     );
                     Status::internal(format!("unexpected error: {err}"))
                 })?;
-            let mfa_enabled = network.mfa_enabled();
             let config = ProtoDeviceConfig {
                 config: Device::create_config(&network, &wireguard_network_device),
                 network_id: network.id,
@@ -128,8 +128,10 @@ pub(crate) async fn build_device_config_response(
                 pubkey: network.pubkey,
                 allowed_ips: network.allowed_ips.as_csv(),
                 dns: network.dns,
-                mfa_enabled,
                 keepalive_interval: network.keepalive_interval,
+                location_mfa: Some(
+                    <LocationMfaType as Into<ProtoLocationMfa>>::into(network.location_mfa).into(),
+                ),
             };
             configs.push(config);
         }
@@ -146,7 +148,6 @@ pub(crate) async fn build_device_config_response(
                 );
                 Status::internal(format!("unexpected error: {err}"))
             })?;
-            let mfa_enabled = network.mfa_enabled();
             if let Some(wireguard_network_device) = wireguard_network_device {
                 let config = ProtoDeviceConfig {
                     config: Device::create_config(&network, &wireguard_network_device),
@@ -157,8 +158,11 @@ pub(crate) async fn build_device_config_response(
                     pubkey: network.pubkey,
                     allowed_ips: network.allowed_ips.as_csv(),
                     dns: network.dns,
-                    mfa_enabled,
                     keepalive_interval: network.keepalive_interval,
+                    location_mfa: Some(
+                        <LocationMfaType as Into<ProtoLocationMfa>>::into(network.location_mfa)
+                            .into(),
+                    ),
                 };
                 configs.push(config);
             }
diff --git a/migrations/20250714203243_add_location_mfa_settings.down.sql b/migrations/20250714203243_add_location_mfa_settings.down.sql
index 4d313c31e7..dab5e81e4e 100644
--- a/migrations/20250714203243_add_location_mfa_settings.down.sql
+++ b/migrations/20250714203243_add_location_mfa_settings.down.sql
@@ -14,3 +14,6 @@ ALTER TABLE wireguard_network ALTER COLUMN "mfa_enabled" SET NOT NULL;
 -- drop new column and type
 ALTER TABLE wireguard_network DROP COLUMN "location_mfa";
 DROP TYPE location_mfa_type;
+
+-- restore `use_openid_for_mfa` setting
+ALTER TABLE settings ADD COLUMN use_openid_for_mfa BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/migrations/20250714203243_add_location_mfa_settings.up.sql b/migrations/20250714203243_add_location_mfa_settings.up.sql
index 2239794f2e..fac4906f23 100644
--- a/migrations/20250714203243_add_location_mfa_settings.up.sql
+++ b/migrations/20250714203243_add_location_mfa_settings.up.sql
@@ -21,3 +21,6 @@ ALTER TABLE wireguard_network ALTER COLUMN "location_mfa" SET NOT NULL;
 
 -- drop the `mfa_enabled` column since it's no longer needed
 ALTER TABLE wireguard_network DROP COLUMN mfa_enabled;
+
+-- remove `use_openid_for_mfa` setting
+ALTER TABLE settings DROP COLUMN use_openid_for_mfa;

From 1a1fbe33ffa55e8843b04845561011520964e039 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Wed, 16 Jul 2025 09:48:37 +0200
Subject: [PATCH 09/26] remove openid mfa setting from frontend

---
 .../enterprise/handlers/openid_providers.rs   |  1 -
 .../tests/integration/openid_login.rs         |  1 -
 web/src/i18n/en/index.ts                      |  5 ----
 web/src/i18n/i18n-types.ts                    | 20 -------------
 .../components/OpenIdGeneralSettings.tsx      | 28 -------------------
 .../components/OpenIdSettingsForm.tsx         |  2 --
 6 files changed, 57 deletions(-)

diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
index 3e12d27a13..97d72f0453 100644
--- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
+++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
@@ -41,7 +41,6 @@ pub struct AddProviderData {
     pub okta_dirsync_client_id: Option<String>,
     pub directory_sync_group_match: Option<String>,
     pub username_handling: OpenidUsernameHandling,
-    pub use_openid_for_mfa: bool,
 }
 
 #[derive(Debug, Deserialize, Serialize)]
diff --git a/crates/defguard_core/tests/integration/openid_login.rs b/crates/defguard_core/tests/integration/openid_login.rs
index 7f999ccb40..d088318d24 100644
--- a/crates/defguard_core/tests/integration/openid_login.rs
+++ b/crates/defguard_core/tests/integration/openid_login.rs
@@ -56,7 +56,6 @@ async fn test_openid_providers(_: PgPoolOptions, options: PgConnectOptions) {
         okta_private_jwk: None,
         directory_sync_group_match: None,
         username_handling: OpenidUsernameHandling::PruneEmailDomain,
-        use_openid_for_mfa: false,
     };
 
     let response = client
diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts
index 01a7edbd8d..7106a22beb 100644
--- a/web/src/i18n/en/index.ts
+++ b/web/src/i18n/en/index.ts
@@ -1287,11 +1287,6 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
           helper:
             'If this option is enabled, Defguard automatically creates new accounts for users who log in for the first time using an external OpenID provider. Otherwise, the user account must first be created by an administrator.',
         },
-        useOpenIdForMfa: {
-          label: 'Use external OpenID for client MFA',
-          helper:
-            'When the external OpenID SSO Multi-Factor (MFA) process is enabled, users connecting to VPN locations that require MFA will need to authenticate via their browser using the configured provider for each connection. If this setting is disabled, MFA for those VPN locations will be handled through the internal Defguard SSO system. In that case, users must have TOTP or email-based MFA configured in their profile.',
-        },
         usernameHandling: {
           label: 'Username handling',
           helper:
diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts
index ea5d392b94..173a0e55a5 100644
--- a/web/src/i18n/i18n-types.ts
+++ b/web/src/i18n/i18n-types.ts
@@ -3183,16 +3183,6 @@ type RootTranslation = {
 					 */
 					helper: string
 				}
-				useOpenIdForMfa: {
-					/**
-					 * U​s​e​ ​e​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​f​o​r​ ​c​l​i​e​n​t​ ​M​F​A
-					 */
-					label: string
-					/**
-					 * W​h​e​n​ ​t​h​e​ ​e​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​S​S​O​ ​M​u​l​t​i​-​F​a​c​t​o​r​ ​(​M​F​A​)​ ​p​r​o​c​e​s​s​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​c​o​n​n​e​c​t​i​n​g​ ​t​o​ ​V​P​N​ ​l​o​c​a​t​i​o​n​s​ ​t​h​a​t​ ​r​e​q​u​i​r​e​ ​M​F​A​ ​w​i​l​l​ ​n​e​e​d​ ​t​o​ ​a​u​t​h​e​n​t​i​c​a​t​e​ ​v​i​a​ ​t​h​e​i​r​ ​b​r​o​w​s​e​r​ ​u​s​i​n​g​ ​t​h​e​ ​c​o​n​f​i​g​u​r​e​d​ ​p​r​o​v​i​d​e​r​ ​f​o​r​ ​e​a​c​h​ ​c​o​n​n​e​c​t​i​o​n​.​ ​I​f​ ​t​h​i​s​ ​s​e​t​t​i​n​g​ ​i​s​ ​d​i​s​a​b​l​e​d​,​ ​M​F​A​ ​f​o​r​ ​t​h​o​s​e​ ​V​P​N​ ​l​o​c​a​t​i​o​n​s​ ​w​i​l​l​ ​b​e​ ​h​a​n​d​l​e​d​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​i​n​t​e​r​n​a​l​ ​D​e​f​g​u​a​r​d​ ​S​S​O​ ​s​y​s​t​e​m​.​ ​I​n​ ​t​h​a​t​ ​c​a​s​e​,​ ​u​s​e​r​s​ ​m​u​s​t​ ​h​a​v​e​ ​T​O​T​P​ ​o​r​ ​e​m​a​i​l​-​b​a​s​e​d​ ​M​F​A​ ​c​o​n​f​i​g​u​r​e​d​ ​i​n​ ​t​h​e​i​r​ ​p​r​o​f​i​l​e​.
-					 */
-					helper: string
-				}
 				usernameHandling: {
 					/**
 					 * U​s​e​r​n​a​m​e​ ​h​a​n​d​l​i​n​g
@@ -9766,16 +9756,6 @@ export type TranslationFunctions = {
 					 */
 					helper: () => LocalizedString
 				}
-				useOpenIdForMfa: {
-					/**
-					 * Use external OpenID for client MFA
-					 */
-					label: () => LocalizedString
-					/**
-					 * When the external OpenID SSO Multi-Factor (MFA) process is enabled, users connecting to VPN locations that require MFA will need to authenticate via their browser using the configured provider for each connection. If this setting is disabled, MFA for those VPN locations will be handled through the internal Defguard SSO system. In that case, users must have TOTP or email-based MFA configured in their profile.
-					 */
-					helper: () => LocalizedString
-				}
 				usernameHandling: {
 					/**
 					 * Username handling
diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx
index 90bd9df092..4722287be4 100644
--- a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx
+++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx
@@ -22,14 +22,6 @@ export const OpenIdGeneralSettings = ({ isLoading }: { isLoading: boolean }) =>
     control,
     name: 'create_account',
   }) as boolean;
-  const use_openid_for_mfa = useWatch({
-    control,
-    name: 'use_openid_for_mfa',
-  }) as boolean;
-  const providerName = useWatch({
-    control,
-    name: 'name',
-  }) as string;
 
   const options: SelectOption<UsernameHandling>[] = useMemo(
     () => [
@@ -52,10 +44,6 @@ export const OpenIdGeneralSettings = ({ isLoading }: { isLoading: boolean }) =>
     [localLL.general.usernameHandling.options],
   );
 
-  const providerConfigured = useMemo(() => {
-    return providerName !== '';
-  }, [providerName]);
-
   return (
     <div id="general-settings">
       <div className="subsection-header helper-row">
@@ -78,22 +66,6 @@ export const OpenIdGeneralSettings = ({ isLoading }: { isLoading: boolean }) =>
         />
         <Helper>{localLL.general.createAccount.helper()}</Helper>
       </div>
-      <div className="helper-row checkbox-padding">
-        {/* FIXME: Really buggy when using the controller, investigate why */}
-        <LabeledCheckbox
-          label={localLL.general.useOpenIdForMfa.label()}
-          // controller={{
-          //   control,
-          //   name: 'use_openid_for_mfa',
-          // }}
-          value={providerConfigured ? use_openid_for_mfa : false}
-          onChange={(e) => {
-            setValue('use_openid_for_mfa', e);
-          }}
-          disabled={isLoading || !providerConfigured}
-        />
-        <Helper>{localLL.general.useOpenIdForMfa.helper()}</Helper>
-      </div>
       <FormSelect
         controller={{
           control,
diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx
index 25a27cea37..3be2f73a68 100644
--- a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx
+++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx
@@ -108,7 +108,6 @@ export const OpenIdSettingsForm = () => {
           okta_private_jwk: z.string(),
           okta_dirsync_client_id: z.string(),
           directory_sync_group_match: z.string(),
-          use_openid_for_mfa: z.boolean(),
         })
         .superRefine((val, ctx) => {
           if (val.name === '') {
@@ -172,7 +171,6 @@ export const OpenIdSettingsForm = () => {
       okta_dirsync_client_id: '',
       directory_sync_group_match: '',
       username_handling: 'RemoveForbidden',
-      use_openid_for_mfa: false,
     };
 
     if (openidData) {

From 74173add31153f457a8fecd835193a7fe56ae053 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Wed, 16 Jul 2025 09:51:42 +0200
Subject: [PATCH 10/26] update query data

---
 ...2e43982729517479cecc61bdd9bf046da789.json} | 31 +++++++++++++------
 ...1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json} | 31 +++++++++++++------
 ...e1130676812d3c5ed48cffe347e59d930b9b.json} | 29 +++++++++++------
 ...a801fb6b9c61b20240ab9f365473373756ef.json} | 31 +++++++++++++------
 ...d28a14cf37e546dfcabdfd78889dc1ef247f.json} |  7 ++---
 ...eb4518c5dd109f30d620b169ae6f3d751a0d.json} | 31 +++++++++++++------
 ...e04120ebdfc78c0dfac15b15781a85a212c1.json} | 29 +++++++++++------
 ...660bcae531c2b8342ae2feffea7454450f10.json} | 10 ++----
 ...7fe3b88446fba18f94b199e97176adca61d1.json} | 19 +++++++++---
 ...64a3955d56ee1930763d2ed372fd4123e0f7.json} | 19 +++++++++---
 ...290465bf658471b97bc06ec386ff0a976b54.json} | 31 +++++++++++++------
 11 files changed, 180 insertions(+), 88 deletions(-)
 rename .sqlx/{query-977a9511c6ea5d27224f43ef42b9048d069f6e6d041d6b9b9c95a3c0f93bd26c.json => query-0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789.json} (75%)
 rename .sqlx/{query-89c395e2709800a8cbd56bb9aca258b435e1b0c8365af30496523458f09e2390.json => query-12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json} (75%)
 rename .sqlx/{query-02b5696315d44f2febf1fe850071d88892e05437c35c60776ee5d9e3d190931a.json => query-133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b.json} (76%)
 rename .sqlx/{query-86f65d13c3f0221b4de39a7042b578149eca53934959119f61635b276b8b0c9e.json => query-317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef.json} (74%)
 rename .sqlx/{query-f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb.json => query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json} (95%)
 rename .sqlx/{query-81f1d11c1b7a2299b26c25be37d209a191845dabb64ec3390c443501ff531bb3.json => query-429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d.json} (72%)
 rename .sqlx/{query-f0d9d574025f8bf22bd8c2195c3d864bab119c33ee43b6b7d02bf86b16e567c0.json => query-4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1.json} (76%)
 rename .sqlx/{query-2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4.json => query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json} (96%)
 rename .sqlx/{query-f948e46b9f5b331e4101c0983b54eebe29f9dd14e6b875b530171b3607ee3d94.json => query-8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1.json} (52%)
 rename .sqlx/{query-ce3e4369b0e449a7fefd45dd52c86849dd6d17db19db0fbeb833e68eee17d5fe.json => query-a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7.json} (51%)
 rename .sqlx/{query-f65176f7b65f553ddcb0240903de3b8b1951a5c48de9f25b9c921e1967a972b9.json => query-fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54.json} (73%)

diff --git a/.sqlx/query-977a9511c6ea5d27224f43ef42b9048d069f6e6d041d6b9b9c95a3c0f93bd26c.json b/.sqlx/query-0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789.json
similarity index 75%
rename from .sqlx/query-977a9511c6ea5d27224f43ef42b9048d069f6e6d041d6b9b9c95a3c0f93bd26c.json
rename to .sqlx/query-0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789.json
index b011d62d0b..8a61dbe7c5 100644
--- a/.sqlx/query-977a9511c6ea5d27224f43ef42b9048d069f6e6d041d6b9b9c95a3c0f93bd26c.json
+++ b/.sqlx/query-0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow FROM wireguard_network WHERE name = $1",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM wireguard_network WHERE name = $1",
   "describe": {
     "columns": [
       {
@@ -55,28 +55,39 @@
       },
       {
         "ordinal": 10,
-        "name": "mfa_enabled",
-        "type_info": "Bool"
-      },
-      {
-        "ordinal": 11,
         "name": "keepalive_interval",
         "type_info": "Int4"
       },
       {
-        "ordinal": 12,
+        "ordinal": 11,
         "name": "peer_disconnect_threshold",
         "type_info": "Int4"
       },
       {
-        "ordinal": 13,
+        "ordinal": 12,
         "name": "acl_enabled",
         "type_info": "Bool"
       },
       {
-        "ordinal": 14,
+        "ordinal": 13,
         "name": "acl_default_allow",
         "type_info": "Bool"
+      },
+      {
+        "ordinal": 14,
+        "name": "location_mfa: LocationMfaType",
+        "type_info": {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       }
     ],
     "parameters": {
@@ -102,5 +113,5 @@
       false
     ]
   },
-  "hash": "977a9511c6ea5d27224f43ef42b9048d069f6e6d041d6b9b9c95a3c0f93bd26c"
+  "hash": "0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789"
 }
diff --git a/.sqlx/query-89c395e2709800a8cbd56bb9aca258b435e1b0c8365af30496523458f09e2390.json b/.sqlx/query-12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json
similarity index 75%
rename from .sqlx/query-89c395e2709800a8cbd56bb9aca258b435e1b0c8365af30496523458f09e2390.json
rename to .sqlx/query-12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json
index d264ca5233..cdd890b40d 100644
--- a/.sqlx/query-89c395e2709800a8cbd56bb9aca258b435e1b0c8365af30496523458f09e2390.json
+++ b/.sqlx/query-12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow FROM wireguard_network WHERE id IN (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM wireguard_network WHERE id = $1",
   "describe": {
     "columns": [
       {
@@ -55,28 +55,39 @@
       },
       {
         "ordinal": 10,
-        "name": "mfa_enabled",
-        "type_info": "Bool"
-      },
-      {
-        "ordinal": 11,
         "name": "keepalive_interval",
         "type_info": "Int4"
       },
       {
-        "ordinal": 12,
+        "ordinal": 11,
         "name": "peer_disconnect_threshold",
         "type_info": "Int4"
       },
       {
-        "ordinal": 13,
+        "ordinal": 12,
         "name": "acl_enabled",
         "type_info": "Bool"
       },
       {
-        "ordinal": 14,
+        "ordinal": 13,
         "name": "acl_default_allow",
         "type_info": "Bool"
+      },
+      {
+        "ordinal": 14,
+        "name": "location_mfa: LocationMfaType",
+        "type_info": {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       }
     ],
     "parameters": {
@@ -102,5 +113,5 @@
       false
     ]
   },
-  "hash": "89c395e2709800a8cbd56bb9aca258b435e1b0c8365af30496523458f09e2390"
+  "hash": "12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b"
 }
diff --git a/.sqlx/query-02b5696315d44f2febf1fe850071d88892e05437c35c60776ee5d9e3d190931a.json b/.sqlx/query-133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b.json
similarity index 76%
rename from .sqlx/query-02b5696315d44f2febf1fe850071d88892e05437c35c60776ee5d9e3d190931a.json
rename to .sqlx/query-133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b.json
index b2b9ba5ecc..094efeef12 100644
--- a/.sqlx/query-02b5696315d44f2febf1fe850071d88892e05437c35c60776ee5d9e3d190931a.json
+++ b/.sqlx/query-133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"mfa_enabled\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\" FROM \"wireguard_network\" WHERE id = $1",
+  "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa\" \"location_mfa: _\" FROM \"wireguard_network\" WHERE id = $1",
   "describe": {
     "columns": [
       {
@@ -55,28 +55,39 @@
       },
       {
         "ordinal": 10,
-        "name": "mfa_enabled",
+        "name": "acl_enabled",
         "type_info": "Bool"
       },
       {
         "ordinal": 11,
-        "name": "acl_enabled",
+        "name": "acl_default_allow",
         "type_info": "Bool"
       },
       {
         "ordinal": 12,
-        "name": "acl_default_allow",
-        "type_info": "Bool"
+        "name": "keepalive_interval",
+        "type_info": "Int4"
       },
       {
         "ordinal": 13,
-        "name": "keepalive_interval",
+        "name": "peer_disconnect_threshold",
         "type_info": "Int4"
       },
       {
         "ordinal": 14,
-        "name": "peer_disconnect_threshold",
-        "type_info": "Int4"
+        "name": "location_mfa: _",
+        "type_info": {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       }
     ],
     "parameters": {
@@ -102,5 +113,5 @@
       false
     ]
   },
-  "hash": "02b5696315d44f2febf1fe850071d88892e05437c35c60776ee5d9e3d190931a"
+  "hash": "133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b"
 }
diff --git a/.sqlx/query-86f65d13c3f0221b4de39a7042b578149eca53934959119f61635b276b8b0c9e.json b/.sqlx/query-317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef.json
similarity index 74%
rename from .sqlx/query-86f65d13c3f0221b4de39a7042b578149eca53934959119f61635b276b8b0c9e.json
rename to .sqlx/query-317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef.json
index 3583aa6f84..9b6ef47f1f 100644
--- a/.sqlx/query-86f65d13c3f0221b4de39a7042b578149eca53934959119f61635b276b8b0c9e.json
+++ b/.sqlx/query-317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow FROM wireguard_network WHERE mfa_enabled = true",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM wireguard_network WHERE location_mfa != 'disabled'::location_mfa_type",
   "describe": {
     "columns": [
       {
@@ -55,28 +55,39 @@
       },
       {
         "ordinal": 10,
-        "name": "mfa_enabled",
-        "type_info": "Bool"
-      },
-      {
-        "ordinal": 11,
         "name": "keepalive_interval",
         "type_info": "Int4"
       },
       {
-        "ordinal": 12,
+        "ordinal": 11,
         "name": "peer_disconnect_threshold",
         "type_info": "Int4"
       },
       {
-        "ordinal": 13,
+        "ordinal": 12,
         "name": "acl_enabled",
         "type_info": "Bool"
       },
       {
-        "ordinal": 14,
+        "ordinal": 13,
         "name": "acl_default_allow",
         "type_info": "Bool"
+      },
+      {
+        "ordinal": 14,
+        "name": "location_mfa: LocationMfaType",
+        "type_info": {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       }
     ],
     "parameters": {
@@ -100,5 +111,5 @@
       false
     ]
   },
-  "hash": "86f65d13c3f0221b4de39a7042b578149eca53934959119f61635b276b8b0c9e"
+  "hash": "317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef"
 }
diff --git a/.sqlx/query-f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb.json b/.sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json
similarity index 95%
rename from .sqlx/query-f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb.json
rename to .sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json
index afb392ccf3..beabc1823f 100644
--- a/.sqlx/query-f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb.json
+++ b/.sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.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, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password  = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48, use_openid_for_mfa = $49 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, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password  = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48 WHERE id = 1",
   "describe": {
     "columns": [],
     "parameters": {
@@ -84,11 +84,10 @@
               ]
             }
           }
-        },
-        "Bool"
+        }
       ]
     },
     "nullable": []
   },
-  "hash": "f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb"
+  "hash": "3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f"
 }
diff --git a/.sqlx/query-81f1d11c1b7a2299b26c25be37d209a191845dabb64ec3390c443501ff531bb3.json b/.sqlx/query-429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d.json
similarity index 72%
rename from .sqlx/query-81f1d11c1b7a2299b26c25be37d209a191845dabb64ec3390c443501ff531bb3.json
rename to .sqlx/query-429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d.json
index e79348ad65..4746c54236 100644
--- a/.sqlx/query-81f1d11c1b7a2299b26c25be37d209a191845dabb64ec3390c443501ff531bb3.json
+++ b/.sqlx/query-429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow FROM wireguard_network WHERE id = $1",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at,  keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM wireguard_network WHERE id IN (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)",
   "describe": {
     "columns": [
       {
@@ -55,28 +55,39 @@
       },
       {
         "ordinal": 10,
-        "name": "mfa_enabled",
-        "type_info": "Bool"
-      },
-      {
-        "ordinal": 11,
         "name": "keepalive_interval",
         "type_info": "Int4"
       },
       {
-        "ordinal": 12,
+        "ordinal": 11,
         "name": "peer_disconnect_threshold",
         "type_info": "Int4"
       },
       {
-        "ordinal": 13,
+        "ordinal": 12,
         "name": "acl_enabled",
         "type_info": "Bool"
       },
       {
-        "ordinal": 14,
+        "ordinal": 13,
         "name": "acl_default_allow",
         "type_info": "Bool"
+      },
+      {
+        "ordinal": 14,
+        "name": "location_mfa: LocationMfaType",
+        "type_info": {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       }
     ],
     "parameters": {
@@ -102,5 +113,5 @@
       false
     ]
   },
-  "hash": "81f1d11c1b7a2299b26c25be37d209a191845dabb64ec3390c443501ff531bb3"
+  "hash": "429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d"
 }
diff --git a/.sqlx/query-f0d9d574025f8bf22bd8c2195c3d864bab119c33ee43b6b7d02bf86b16e567c0.json b/.sqlx/query-4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1.json
similarity index 76%
rename from .sqlx/query-f0d9d574025f8bf22bd8c2195c3d864bab119c33ee43b6b7d02bf86b16e567c0.json
rename to .sqlx/query-4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1.json
index 6448dcc386..0d28b62520 100644
--- a/.sqlx/query-f0d9d574025f8bf22bd8c2195c3d864bab119c33ee43b6b7d02bf86b16e567c0.json
+++ b/.sqlx/query-4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"mfa_enabled\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\" FROM \"wireguard_network\"",
+  "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa\" \"location_mfa: _\" FROM \"wireguard_network\"",
   "describe": {
     "columns": [
       {
@@ -55,28 +55,39 @@
       },
       {
         "ordinal": 10,
-        "name": "mfa_enabled",
+        "name": "acl_enabled",
         "type_info": "Bool"
       },
       {
         "ordinal": 11,
-        "name": "acl_enabled",
+        "name": "acl_default_allow",
         "type_info": "Bool"
       },
       {
         "ordinal": 12,
-        "name": "acl_default_allow",
-        "type_info": "Bool"
+        "name": "keepalive_interval",
+        "type_info": "Int4"
       },
       {
         "ordinal": 13,
-        "name": "keepalive_interval",
+        "name": "peer_disconnect_threshold",
         "type_info": "Int4"
       },
       {
         "ordinal": 14,
-        "name": "peer_disconnect_threshold",
-        "type_info": "Int4"
+        "name": "location_mfa: _",
+        "type_info": {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       }
     ],
     "parameters": {
@@ -100,5 +111,5 @@
       false
     ]
   },
-  "hash": "f0d9d574025f8bf22bd8c2195c3d864bab119c33ee43b6b7d02bf86b16e567c0"
+  "hash": "4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1"
 }
diff --git a/.sqlx/query-2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4.json b/.sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json
similarity index 96%
rename from .sqlx/query-2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4.json
rename to .sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json
index 53fdfdd55c..9f60163eac 100644
--- a/.sqlx/query-2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4.json
+++ b/.sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.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, 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: SyncStatus\", 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, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\", use_openid_for_mfa 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, 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: SyncStatus\", 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, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" FROM \"settings\" WHERE id = 1",
   "describe": {
     "columns": [
       {
@@ -274,11 +274,6 @@
             }
           }
         }
-      },
-      {
-        "ordinal": 48,
-        "name": "use_openid_for_mfa",
-        "type_info": "Bool"
       }
     ],
     "parameters": {
@@ -332,9 +327,8 @@
       false,
       true,
       false,
-      false,
       false
     ]
   },
-  "hash": "2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4"
+  "hash": "7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10"
 }
diff --git a/.sqlx/query-f948e46b9f5b331e4101c0983b54eebe29f9dd14e6b875b530171b3607ee3d94.json b/.sqlx/query-8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1.json
similarity index 52%
rename from .sqlx/query-f948e46b9f5b331e4101c0983b54eebe29f9dd14e6b875b530171b3607ee3d94.json
rename to .sqlx/query-8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1.json
index 5478f802b6..73802a08e9 100644
--- a/.sqlx/query-f948e46b9f5b331e4101c0983b54eebe29f9dd14e6b875b530171b3607ee3d94.json
+++ b/.sqlx/query-8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "UPDATE \"wireguard_network\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"pubkey\" = $5,\"prvkey\" = $6,\"endpoint\" = $7,\"dns\" = $8,\"allowed_ips\" = $9,\"connected_at\" = $10,\"mfa_enabled\" = $11,\"acl_enabled\" = $12,\"acl_default_allow\" = $13,\"keepalive_interval\" = $14,\"peer_disconnect_threshold\" = $15 WHERE id = $1",
+  "query": "UPDATE \"wireguard_network\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"pubkey\" = $5,\"prvkey\" = $6,\"endpoint\" = $7,\"dns\" = $8,\"allowed_ips\" = $9,\"connected_at\" = $10,\"acl_enabled\" = $11,\"acl_default_allow\" = $12,\"keepalive_interval\" = $13,\"peer_disconnect_threshold\" = $14,\"location_mfa\" = $15 WHERE id = $1",
   "describe": {
     "columns": [],
     "parameters": {
@@ -17,12 +17,23 @@
         "Timestamp",
         "Bool",
         "Bool",
-        "Bool",
         "Int4",
-        "Int4"
+        "Int4",
+        {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       ]
     },
     "nullable": []
   },
-  "hash": "f948e46b9f5b331e4101c0983b54eebe29f9dd14e6b875b530171b3607ee3d94"
+  "hash": "8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1"
 }
diff --git a/.sqlx/query-ce3e4369b0e449a7fefd45dd52c86849dd6d17db19db0fbeb833e68eee17d5fe.json b/.sqlx/query-a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7.json
similarity index 51%
rename from .sqlx/query-ce3e4369b0e449a7fefd45dd52c86849dd6d17db19db0fbeb833e68eee17d5fe.json
rename to .sqlx/query-a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7.json
index 8b46245120..8d902bef66 100644
--- a/.sqlx/query-ce3e4369b0e449a7fefd45dd52c86849dd6d17db19db0fbeb833e68eee17d5fe.json
+++ b/.sqlx/query-a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "INSERT INTO \"wireguard_network\" (\"name\",\"address\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\",\"connected_at\",\"mfa_enabled\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id",
+  "query": "INSERT INTO \"wireguard_network\" (\"name\",\"address\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id",
   "describe": {
     "columns": [
       {
@@ -22,14 +22,25 @@
         "Timestamp",
         "Bool",
         "Bool",
-        "Bool",
         "Int4",
-        "Int4"
+        "Int4",
+        {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       ]
     },
     "nullable": [
       false
     ]
   },
-  "hash": "ce3e4369b0e449a7fefd45dd52c86849dd6d17db19db0fbeb833e68eee17d5fe"
+  "hash": "a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7"
 }
diff --git a/.sqlx/query-f65176f7b65f553ddcb0240903de3b8b1951a5c48de9f25b9c921e1967a972b9.json b/.sqlx/query-fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54.json
similarity index 73%
rename from .sqlx/query-f65176f7b65f553ddcb0240903de3b8b1951a5c48de9f25b9c921e1967a972b9.json
rename to .sqlx/query-fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54.json
index f06c3c2ee1..7b06fae1e2 100644
--- a/.sqlx/query-f65176f7b65f553ddcb0240903de3b8b1951a5c48de9f25b9c921e1967a972b9.json
+++ b/.sqlx/query-fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, mfa_enabled, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow FROM aclrulenetwork r JOIN wireguard_network n ON n.id = r.network_id WHERE r.rule_id = $1",
+  "query": "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM aclrulenetwork r JOIN wireguard_network n ON n.id = r.network_id WHERE r.rule_id = $1",
   "describe": {
     "columns": [
       {
@@ -55,28 +55,39 @@
       },
       {
         "ordinal": 10,
-        "name": "mfa_enabled",
-        "type_info": "Bool"
-      },
-      {
-        "ordinal": 11,
         "name": "keepalive_interval",
         "type_info": "Int4"
       },
       {
-        "ordinal": 12,
+        "ordinal": 11,
         "name": "peer_disconnect_threshold",
         "type_info": "Int4"
       },
       {
-        "ordinal": 13,
+        "ordinal": 12,
         "name": "acl_enabled",
         "type_info": "Bool"
       },
       {
-        "ordinal": 14,
+        "ordinal": 13,
         "name": "acl_default_allow",
         "type_info": "Bool"
+      },
+      {
+        "ordinal": 14,
+        "name": "location_mfa: LocationMfaType",
+        "type_info": {
+          "Custom": {
+            "name": "location_mfa_type",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
       }
     ],
     "parameters": {
@@ -102,5 +113,5 @@
       false
     ]
   },
-  "hash": "f65176f7b65f553ddcb0240903de3b8b1951a5c48de9f25b9c921e1967a972b9"
+  "hash": "fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54"
 }

From 5cae6799dee3d1573532c312cd2ce42fc4760a00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Wed, 16 Jul 2025 10:24:09 +0200
Subject: [PATCH 11/26] update protos

---
 proto | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proto b/proto
index 9fc4e466ad..8dcc9d83a8 160000
--- a/proto
+++ b/proto
@@ -1 +1 @@
-Subproject commit 9fc4e466adae627ae332560c09df871c703b91a3
+Subproject commit 8dcc9d83a8cd83a2f8e1852be5d47f1fe233f3a8

From 48eac8c6fe38c220d172d996cd236e47426ca414 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Wed, 16 Jul 2025 10:29:51 +0200
Subject: [PATCH 12/26] handle restored field

---
 crates/defguard_core/src/grpc/enrollment.rs | 4 ++++
 crates/defguard_core/src/grpc/utils.rs      | 8 ++++++++
 2 files changed, 12 insertions(+)

diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs
index 2dd4862547..8c79d47828 100644
--- a/crates/defguard_core/src/grpc/enrollment.rs
+++ b/crates/defguard_core/src/grpc/enrollment.rs
@@ -851,6 +851,8 @@ impl InitialUserInfo {
 
 impl From<DeviceConfig> for ProtoDeviceConfig {
     fn from(config: DeviceConfig) -> Self {
+        // used by pre-1.5 clients which don't support external MFA
+        let mfa_enabled = config.location_mfa == LocationMfaType::Internal;
         Self {
             network_id: config.network_id,
             network_name: config.network_name,
@@ -861,6 +863,8 @@ impl From<DeviceConfig> for ProtoDeviceConfig {
             allowed_ips: config.allowed_ips.as_csv(),
             dns: config.dns,
             keepalive_interval: config.keepalive_interval,
+            #[allow(deprecated)]
+            mfa_enabled,
             location_mfa: Some(
                 <LocationMfaType as Into<ProtoLocationMfa>>::into(config.location_mfa).into(),
             ),
diff --git a/crates/defguard_core/src/grpc/utils.rs b/crates/defguard_core/src/grpc/utils.rs
index e5df767e1d..938498b4b2 100644
--- a/crates/defguard_core/src/grpc/utils.rs
+++ b/crates/defguard_core/src/grpc/utils.rs
@@ -119,6 +119,8 @@ pub(crate) async fn build_device_config_response(
                     );
                     Status::internal(format!("unexpected error: {err}"))
                 })?;
+            // used by pre-1.5 clients which don't support external MFA
+            let mfa_enabled = network.location_mfa == LocationMfaType::Internal;
             let config = ProtoDeviceConfig {
                 config: Device::create_config(&network, &wireguard_network_device),
                 network_id: network.id,
@@ -129,6 +131,8 @@ pub(crate) async fn build_device_config_response(
                 allowed_ips: network.allowed_ips.as_csv(),
                 dns: network.dns,
                 keepalive_interval: network.keepalive_interval,
+                #[allow(deprecated)]
+                mfa_enabled,
                 location_mfa: Some(
                     <LocationMfaType as Into<ProtoLocationMfa>>::into(network.location_mfa).into(),
                 ),
@@ -148,6 +152,8 @@ pub(crate) async fn build_device_config_response(
                 );
                 Status::internal(format!("unexpected error: {err}"))
             })?;
+            // used by pre-1.5 clients which don't support external MFA
+            let mfa_enabled = network.location_mfa == LocationMfaType::Internal;
             if let Some(wireguard_network_device) = wireguard_network_device {
                 let config = ProtoDeviceConfig {
                     config: Device::create_config(&network, &wireguard_network_device),
@@ -159,6 +165,8 @@ pub(crate) async fn build_device_config_response(
                     allowed_ips: network.allowed_ips.as_csv(),
                     dns: network.dns,
                     keepalive_interval: network.keepalive_interval,
+                    #[allow(deprecated)]
+                    mfa_enabled,
                     location_mfa: Some(
                         <LocationMfaType as Into<ProtoLocationMfa>>::into(network.location_mfa)
                             .into(),

From d56177826c21864996a90cd3f3114eabe7f8a727 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Thu, 17 Jul 2025 12:35:43 +0200
Subject: [PATCH 13/26] handle updated field naming

---
 crates/defguard_core/src/db/models/device.rs  | 14 ++---
 .../defguard_core/src/db/models/wireguard.rs  | 52 ++++++++++---------
 .../src/enterprise/db/models/acl.rs           |  4 +-
 .../src/enterprise/db/models/acl/tests.rs     |  4 +-
 .../src/enterprise/directory_sync/mod.rs      |  4 +-
 crates/defguard_core/src/grpc/enrollment.rs   | 11 ++--
 crates/defguard_core/src/grpc/utils.rs        | 23 ++++----
 .../defguard_core/src/handlers/wireguard.rs   |  8 +--
 crates/defguard_core/src/lib.rs               |  8 +--
 crates/defguard_core/src/wg_config.rs         |  4 +-
 .../src/wireguard_peer_disconnect.rs          |  6 +--
 crates/defguard_core/tests/integration/acl.rs |  6 +--
 .../tests/integration/wireguard.rs            |  4 +-
 .../integration/wireguard_network_import.rs   |  4 +-
 ...4203243_add_location_mfa_settings.down.sql |  6 +--
 ...714203243_add_location_mfa_settings.up.sql | 12 ++---
 proto                                         |  2 +-
 web/src/i18n/en/index.ts                      |  2 +-
 web/src/i18n/i18n-types.ts                    |  4 +-
 .../NetworkEditForm/NetworkEditForm.tsx       | 10 ++--
 .../WizardNetworkConfiguration.tsx            |  8 +--
 web/src/pages/wizard/hooks/useWizardStore.ts  |  6 +--
 .../FormLocationMfaModeSelect.tsx}            | 26 +++++-----
 web/src/shared/types.ts                       |  4 +-
 24 files changed, 121 insertions(+), 111 deletions(-)
 rename web/src/shared/components/Form/{FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx => FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx} (55%)

diff --git a/crates/defguard_core/src/db/models/device.rs b/crates/defguard_core/src/db/models/device.rs
index 07d18e2348..84785f64f5 100644
--- a/crates/defguard_core/src/db/models/device.rs
+++ b/crates/defguard_core/src/db/models/device.rs
@@ -21,7 +21,7 @@ use utoipa::ToSchema;
 
 use super::{
     error::ModelError,
-    wireguard::{LocationMfaType, NetworkAddressError, WIREGUARD_MAX_HANDSHAKE, WireguardNetwork},
+    wireguard::{LocationMfaMode, NetworkAddressError, WIREGUARD_MAX_HANDSHAKE, WireguardNetwork},
 };
 use crate::{
     AsCsv, KEY_LENGTH,
@@ -41,7 +41,7 @@ pub struct DeviceConfig {
     pub(crate) pubkey: String,
     pub(crate) dns: Option<String>,
     pub(crate) keepalive_interval: i32,
-    pub(crate) location_mfa: LocationMfaType,
+    pub(crate) location_mfa_mode: LocationMfaMode,
 }
 
 // The type of a device:
@@ -500,7 +500,7 @@ impl WireguardNetworkDevice {
             WireguardNetwork,
             "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
             connected_at, keepalive_interval, peer_disconnect_threshold, \
-            acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
+            acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \
             FROM wireguard_network WHERE id = $1",
             self.wireguard_network_id
         )
@@ -693,7 +693,7 @@ impl Device<Id> {
             pubkey: network.pubkey.clone(),
             dns: network.dns.clone(),
             keepalive_interval: network.keepalive_interval,
-            location_mfa: network.location_mfa.clone(),
+            location_mfa_mode: network.location_mfa_mode.clone(),
         };
 
         Ok((device_network_info, device_config))
@@ -726,7 +726,7 @@ impl Device<Id> {
             pubkey: network.pubkey.clone(),
             dns: network.dns.clone(),
             keepalive_interval: network.keepalive_interval,
-            location_mfa: network.location_mfa.clone(),
+            location_mfa_mode: network.location_mfa_mode.clone(),
         };
 
         Ok((device_network_info, device_config))
@@ -788,7 +788,7 @@ impl Device<Id> {
                     pubkey: network.pubkey,
                     dns: network.dns,
                     keepalive_interval: network.keepalive_interval,
-                    location_mfa: network.location_mfa.clone(),
+                    location_mfa_mode: network.location_mfa_mode.clone(),
                 });
             }
         }
@@ -935,7 +935,7 @@ impl Device<Id> {
             WireguardNetwork,
             "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
             connected_at,  keepalive_interval, peer_disconnect_threshold, \
-            acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
+            acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \
             FROM wireguard_network WHERE id IN \
             (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)",
             self.id
diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs
index c7854bcfc0..4719d15e4d 100644
--- a/crates/defguard_core/src/db/models/wireguard.rs
+++ b/crates/defguard_core/src/db/models/wireguard.rs
@@ -35,7 +35,9 @@ use crate::{
     grpc::{
         GatewayState,
         gateway::{Peer, send_multiple_wireguard_events},
-        proto::{enterprise::firewall::FirewallConfig, proxy::LocationMfa as ProtoLocationMfa},
+        proto::{
+            enterprise::firewall::FirewallConfig, proxy::LocationMfaMode as ProtoLocationMfaMode,
+        },
     },
     wg_config::ImportedDevice,
 };
@@ -84,30 +86,32 @@ pub enum GatewayEvent {
 }
 
 #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, ToSchema, Type)]
-#[sqlx(type_name = "location_mfa_type", rename_all = "lowercase")]
+#[sqlx(type_name = "location_mfa_mode", rename_all = "lowercase")]
 #[serde(rename_all = "lowercase")]
-pub enum LocationMfaType {
+pub enum LocationMfaMode {
     #[default]
     Disabled,
     Internal,
     External,
 }
 
-impl From<ProtoLocationMfa> for LocationMfaType {
-    fn from(value: ProtoLocationMfa) -> Self {
+impl From<ProtoLocationMfaMode> for LocationMfaMode {
+    fn from(value: ProtoLocationMfaMode) -> Self {
         match value {
-            ProtoLocationMfa::Unspecified | ProtoLocationMfa::Disabled => LocationMfaType::Disabled,
-            ProtoLocationMfa::Internal => LocationMfaType::Internal,
-            ProtoLocationMfa::External => LocationMfaType::External,
+            ProtoLocationMfaMode::Unspecified | ProtoLocationMfaMode::Disabled => {
+                LocationMfaMode::Disabled
+            }
+            ProtoLocationMfaMode::Internal => LocationMfaMode::Internal,
+            ProtoLocationMfaMode::External => LocationMfaMode::External,
         }
     }
 }
-impl From<LocationMfaType> for ProtoLocationMfa {
-    fn from(value: LocationMfaType) -> Self {
+impl From<LocationMfaMode> for ProtoLocationMfaMode {
+    fn from(value: LocationMfaMode) -> Self {
         match value {
-            LocationMfaType::Disabled => ProtoLocationMfa::Disabled,
-            LocationMfaType::Internal => ProtoLocationMfa::Internal,
-            LocationMfaType::External => ProtoLocationMfa::External,
+            LocationMfaMode::Disabled => ProtoLocationMfaMode::Disabled,
+            LocationMfaMode::Internal => ProtoLocationMfaMode::Internal,
+            LocationMfaMode::External => ProtoLocationMfaMode::External,
         }
     }
 }
@@ -136,7 +140,7 @@ pub struct WireguardNetwork<I = NoId> {
     pub keepalive_interval: i32,
     pub peer_disconnect_threshold: i32,
     #[model(enum)]
-    pub location_mfa: LocationMfaType,
+    pub location_mfa_mode: LocationMfaMode,
 }
 
 pub struct WireguardKey {
@@ -174,7 +178,7 @@ impl Default for WireguardNetwork<Id> {
             peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD,
             acl_default_allow: false,
             acl_enabled: false,
-            location_mfa: LocationMfaType::default(),
+            location_mfa_mode: LocationMfaMode::default(),
         }
     }
 }
@@ -231,7 +235,7 @@ impl WireguardNetwork {
         peer_disconnect_threshold: i32,
         acl_enabled: bool,
         acl_default_allow: bool,
-        location_mfa: LocationMfaType,
+        location_mfa_mode: LocationMfaMode,
     ) -> Result<Self, WireguardNetworkError> {
         let prvkey = StaticSecret::random_from_rng(OsRng);
         let pubkey = PublicKey::from(&prvkey);
@@ -251,7 +255,7 @@ impl WireguardNetwork {
             peer_disconnect_threshold,
             acl_enabled,
             acl_default_allow,
-            location_mfa,
+            location_mfa_mode,
         })
     }
 
@@ -282,7 +286,7 @@ impl WireguardNetwork<Id> {
             WireguardNetwork,
             "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
             connected_at, keepalive_interval, peer_disconnect_threshold, \
-            acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
+            acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \
             FROM wireguard_network WHERE name = $1",
             name
         )
@@ -1260,9 +1264,9 @@ impl WireguardNetwork<Id> {
     }
 
     pub fn mfa_enabled(&self) -> bool {
-        match self.location_mfa {
-            LocationMfaType::Internal | LocationMfaType::External => true,
-            LocationMfaType::Disabled => false,
+        match self.location_mfa_mode {
+            LocationMfaMode::Internal | LocationMfaMode::External => true,
+            LocationMfaMode::Disabled => false,
         }
     }
 }
@@ -1285,7 +1289,7 @@ impl Default for WireguardNetwork {
             peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD,
             acl_enabled: false,
             acl_default_allow: false,
-            location_mfa: LocationMfaType::default(),
+            location_mfa_mode: LocationMfaMode::default(),
         }
     }
 }
@@ -1997,7 +2001,7 @@ mod test {
             300,
             false,
             false,
-            LocationMfaType::Disabled,
+            LocationMfaMode::Disabled,
         )
         .unwrap()
         .save(&pool)
@@ -2129,7 +2133,7 @@ mod test {
             300,
             false,
             false,
-            LocationMfaType::Disabled,
+            LocationMfaMode::Disabled,
         )
         .unwrap()
         .save(&pool)
diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs
index 24817a87e1..3982a802f0 100644
--- a/crates/defguard_core/src/enterprise/db/models/acl.rs
+++ b/crates/defguard_core/src/enterprise/db/models/acl.rs
@@ -19,7 +19,7 @@ use crate::{
     appstate::AppState,
     db::{
         Device, GatewayEvent, Group, Id, NoId, User, WireguardNetwork,
-        models::wireguard::LocationMfaType,
+        models::wireguard::LocationMfaMode,
     },
     enterprise::{
         firewall::FirewallError,
@@ -908,7 +908,7 @@ impl AclRule<Id> {
                 WireguardNetwork,
                 "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
                 connected_at, keepalive_interval, peer_disconnect_threshold, \
-                acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
+                acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \
                 FROM aclrulenetwork r \
                 JOIN wireguard_network n \
                 ON n.id = r.network_id \
diff --git a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs
index 391f0f97fd..211c8cc769 100644
--- a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs
+++ b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs
@@ -181,7 +181,7 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) {
         100,
         false,
         false,
-        LocationMfaType::Disabled,
+        LocationMfaMode::Disabled,
     )
     .unwrap()
     .save(&pool)
@@ -198,7 +198,7 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) {
         200,
         false,
         false,
-        LocationMfaType::Disabled,
+        LocationMfaMode::Disabled,
     )
     .unwrap()
     .save(&pool)
diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs
index 8b6df782fc..fef3db04c3 100644
--- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs
+++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs
@@ -913,7 +913,7 @@ mod test {
             Device, Session, SessionState, Settings, WireguardNetwork,
             models::{
                 device::DeviceType, settings::initialize_current_settings,
-                wireguard::LocationMfaType,
+                wireguard::LocationMfaMode,
             },
             setup_pool,
         },
@@ -955,7 +955,7 @@ mod test {
             32,
             false,
             false,
-            LocationMfaType::Disabled,
+            LocationMfaMode::Disabled,
         )
         .unwrap()
         .save(pool)
diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs
index 8c79d47828..5f40b68e29 100644
--- a/crates/defguard_core/src/grpc/enrollment.rs
+++ b/crates/defguard_core/src/grpc/enrollment.rs
@@ -23,7 +23,7 @@ use crate::{
             device::{DeviceConfig, DeviceInfo, DeviceType},
             enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError},
             polling_token::PollingToken,
-            wireguard::LocationMfaType,
+            wireguard::LocationMfaMode,
         },
     },
     enterprise::{
@@ -33,7 +33,7 @@ use crate::{
     },
     events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, EnrollmentEvent},
     grpc::{
-        proto::proxy::LocationMfa as ProtoLocationMfa,
+        proto::proxy::LocationMfaMode as ProtoLocationMfaMode,
         utils::{build_device_config_response, new_polling_token, parse_client_info},
     },
     handlers::{mail::send_new_device_added_email, user::check_password_strength},
@@ -852,7 +852,7 @@ impl InitialUserInfo {
 impl From<DeviceConfig> for ProtoDeviceConfig {
     fn from(config: DeviceConfig) -> Self {
         // used by pre-1.5 clients which don't support external MFA
-        let mfa_enabled = config.location_mfa == LocationMfaType::Internal;
+        let mfa_enabled = config.location_mfa_mode == LocationMfaMode::Internal;
         Self {
             network_id: config.network_id,
             network_name: config.network_name,
@@ -865,8 +865,9 @@ impl From<DeviceConfig> for ProtoDeviceConfig {
             keepalive_interval: config.keepalive_interval,
             #[allow(deprecated)]
             mfa_enabled,
-            location_mfa: Some(
-                <LocationMfaType as Into<ProtoLocationMfa>>::into(config.location_mfa).into(),
+            location_mfa_mode: Some(
+                <LocationMfaMode as Into<ProtoLocationMfaMode>>::into(config.location_mfa_mode)
+                    .into(),
             ),
         }
     }
diff --git a/crates/defguard_core/src/grpc/utils.rs b/crates/defguard_core/src/grpc/utils.rs
index 938498b4b2..45ba181913 100644
--- a/crates/defguard_core/src/grpc/utils.rs
+++ b/crates/defguard_core/src/grpc/utils.rs
@@ -14,13 +14,13 @@ use crate::{
         models::{
             device::{DeviceType, WireguardNetworkDevice},
             polling_token::PollingToken,
-            wireguard::{LocationMfaType, WireguardNetwork},
+            wireguard::{LocationMfaMode, WireguardNetwork},
         },
     },
     enterprise::db::models::{
         enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider,
     },
-    grpc::proto::proxy::LocationMfa as ProtoLocationMfa,
+    grpc::proto::proxy::LocationMfaMode as ProtoLocationMfaMode,
 };
 
 // Create a new token for configuration polling.
@@ -120,7 +120,7 @@ pub(crate) async fn build_device_config_response(
                     Status::internal(format!("unexpected error: {err}"))
                 })?;
             // used by pre-1.5 clients which don't support external MFA
-            let mfa_enabled = network.location_mfa == LocationMfaType::Internal;
+            let mfa_enabled = network.location_mfa_mode == LocationMfaMode::Internal;
             let config = ProtoDeviceConfig {
                 config: Device::create_config(&network, &wireguard_network_device),
                 network_id: network.id,
@@ -133,8 +133,11 @@ pub(crate) async fn build_device_config_response(
                 keepalive_interval: network.keepalive_interval,
                 #[allow(deprecated)]
                 mfa_enabled,
-                location_mfa: Some(
-                    <LocationMfaType as Into<ProtoLocationMfa>>::into(network.location_mfa).into(),
+                location_mfa_mode: Some(
+                    <LocationMfaMode as Into<ProtoLocationMfaMode>>::into(
+                        network.location_mfa_mode,
+                    )
+                    .into(),
                 ),
             };
             configs.push(config);
@@ -153,7 +156,7 @@ pub(crate) async fn build_device_config_response(
                 Status::internal(format!("unexpected error: {err}"))
             })?;
             // used by pre-1.5 clients which don't support external MFA
-            let mfa_enabled = network.location_mfa == LocationMfaType::Internal;
+            let mfa_enabled = network.location_mfa_mode == LocationMfaMode::Internal;
             if let Some(wireguard_network_device) = wireguard_network_device {
                 let config = ProtoDeviceConfig {
                     config: Device::create_config(&network, &wireguard_network_device),
@@ -167,9 +170,11 @@ pub(crate) async fn build_device_config_response(
                     keepalive_interval: network.keepalive_interval,
                     #[allow(deprecated)]
                     mfa_enabled,
-                    location_mfa: Some(
-                        <LocationMfaType as Into<ProtoLocationMfa>>::into(network.location_mfa)
-                            .into(),
+                    location_mfa_mode: Some(
+                        <LocationMfaMode as Into<ProtoLocationMfaMode>>::into(
+                            network.location_mfa_mode,
+                        )
+                        .into(),
                     ),
                 };
                 configs.push(config);
diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs
index cbb599301b..6e8e0bacda 100644
--- a/crates/defguard_core/src/handlers/wireguard.rs
+++ b/crates/defguard_core/src/handlers/wireguard.rs
@@ -30,7 +30,7 @@ use crate::{
                 WireguardNetworkDevice,
             },
             wireguard::{
-                DateTimeAggregation, LocationMfaType, MappedDevice, WireguardDeviceStatsRow,
+                DateTimeAggregation, LocationMfaMode, MappedDevice, WireguardDeviceStatsRow,
                 WireguardNetworkInfo, WireguardNetworkStats, WireguardUserStatsRow, networks_stats,
             },
         },
@@ -79,7 +79,7 @@ pub struct WireguardNetworkData {
     pub peer_disconnect_threshold: i32,
     pub acl_enabled: bool,
     pub acl_default_allow: bool,
-    pub location_mfa: LocationMfaType,
+    pub location_mfa_mode: LocationMfaMode,
 }
 
 impl WireguardNetworkData {
@@ -149,7 +149,7 @@ pub(crate) async fn create_network(
         data.peer_disconnect_threshold,
         data.acl_enabled,
         data.acl_default_allow,
-        data.location_mfa,
+        data.location_mfa_mode,
     )
     .map_err(|_| WebError::Serialization("Invalid network address".into()))?;
 
@@ -237,7 +237,7 @@ pub(crate) async fn modify_network(
     network.peer_disconnect_threshold = data.peer_disconnect_threshold;
     network.acl_enabled = data.acl_enabled;
     network.acl_default_allow = data.acl_default_allow;
-    network.location_mfa = data.location_mfa;
+    network.location_mfa_mode = data.location_mfa_mode;
 
     network.save(&mut *transaction).await?;
     network
diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs
index dbf540661a..1f8d578d1e 100644
--- a/crates/defguard_core/src/lib.rs
+++ b/crates/defguard_core/src/lib.rs
@@ -13,7 +13,7 @@ use axum::{
     routing::{delete, get, patch, post, put},
     serve,
 };
-use db::models::{device::DeviceType, wireguard::LocationMfaType};
+use db::models::{device::DeviceType, wireguard::LocationMfaMode};
 use defguard_web_ui::{index, svg, web_asset};
 use enterprise::{
     handlers::{
@@ -738,7 +738,7 @@ pub async fn init_dev_env(config: &DefGuardConfig) {
             DEFAULT_DISCONNECT_THRESHOLD,
             false,
             false,
-            LocationMfaType::Disabled,
+            LocationMfaMode::Disabled,
         )
         .expect("Could not create network");
         network.pubkey = "zGMeVGm9HV9I4wSKF9AXmYnnAIhDySyqLMuKpcfIaQo=".to_string();
@@ -837,7 +837,7 @@ pub async fn init_vpn_location(
                 DEFAULT_DISCONNECT_THRESHOLD,
                 false,
                 false,
-                LocationMfaType::Disabled,
+                LocationMfaMode::Disabled,
             )?
             .save(&mut *transaction)
             .await?;
@@ -876,7 +876,7 @@ pub async fn init_vpn_location(
             DEFAULT_DISCONNECT_THRESHOLD,
             false,
             false,
-            LocationMfaType::Disabled,
+            LocationMfaMode::Disabled,
         )?
         .save(pool)
         .await?
diff --git a/crates/defguard_core/src/wg_config.rs b/crates/defguard_core/src/wg_config.rs
index 651d33123e..f634073b9b 100644
--- a/crates/defguard_core/src/wg_config.rs
+++ b/crates/defguard_core/src/wg_config.rs
@@ -10,7 +10,7 @@ use crate::{
     db::{
         Device, WireguardNetwork,
         models::wireguard::{
-            DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaType,
+            DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaMode,
             WireguardNetworkError,
         },
     },
@@ -111,7 +111,7 @@ pub(crate) fn parse_wireguard_config(
         DEFAULT_DISCONNECT_THRESHOLD,
         false,
         false,
-        LocationMfaType::Disabled,
+        LocationMfaMode::Disabled,
     )?;
     network.pubkey = pubkey;
     network.prvkey = prvkey.to_string();
diff --git a/crates/defguard_core/src/wireguard_peer_disconnect.rs b/crates/defguard_core/src/wireguard_peer_disconnect.rs
index 867faa7aea..e0e0dccd54 100644
--- a/crates/defguard_core/src/wireguard_peer_disconnect.rs
+++ b/crates/defguard_core/src/wireguard_peer_disconnect.rs
@@ -27,7 +27,7 @@ use crate::{
         models::{
             device::{DeviceInfo, DeviceNetworkInfo, DeviceType, WireguardNetworkDevice},
             error::ModelError,
-            wireguard::{LocationMfaType, WireguardNetworkError},
+            wireguard::{LocationMfaMode, WireguardNetworkError},
         },
     },
     events::{InternalEvent, InternalEventContext},
@@ -97,8 +97,8 @@ pub async fn run_periodic_peer_disconnect(
             "SELECT \
                 id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
                 connected_at, keepalive_interval, peer_disconnect_threshold, \
-                acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" \
-            FROM wireguard_network WHERE location_mfa != 'disabled'::location_mfa_type",
+                acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \
+            FROM wireguard_network WHERE location_mfa_mode != 'disabled'::location_mfa_mode",
         )
         .fetch_all(&pool)
         .await?;
diff --git a/crates/defguard_core/tests/integration/acl.rs b/crates/defguard_core/tests/integration/acl.rs
index 0823023625..b1a1b223a4 100644
--- a/crates/defguard_core/tests/integration/acl.rs
+++ b/crates/defguard_core/tests/integration/acl.rs
@@ -3,7 +3,7 @@ use defguard_core::{
     db::{
         Device, Group, Id, User, WireguardNetwork,
         models::{
-            device::DeviceType, settings::initialize_current_settings, wireguard::LocationMfaType,
+            device::DeviceType, settings::initialize_current_settings, wireguard::LocationMfaMode,
         },
     },
     enterprise::{
@@ -424,7 +424,7 @@ async fn test_related_objects(_: PgPoolOptions, options: PgConnectOptions) {
             100,
             false,
             false,
-            LocationMfaType::Disabled,
+            LocationMfaMode::Disabled,
         )
         .unwrap()
         .save(&pool)
@@ -765,7 +765,7 @@ async fn test_rule_delete_state_applied(_: PgPoolOptions, options: PgConnectOpti
         100,
         false,
         false,
-        LocationMfaType::Disabled,
+        LocationMfaMode::Disabled,
     )
     .unwrap()
     .save(&pool)
diff --git a/crates/defguard_core/tests/integration/wireguard.rs b/crates/defguard_core/tests/integration/wireguard.rs
index 572e3b1bc5..1e63c93daa 100644
--- a/crates/defguard_core/tests/integration/wireguard.rs
+++ b/crates/defguard_core/tests/integration/wireguard.rs
@@ -6,7 +6,7 @@ use defguard_core::{
         models::{
             device::WireguardNetworkDevice,
             wireguard::{
-                DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaType,
+                DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaMode,
             },
         },
     },
@@ -62,7 +62,7 @@ async fn test_network(_: PgPoolOptions, options: PgConnectOptions) {
         peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD,
         acl_enabled: false,
         acl_default_allow: false,
-        location_mfa: LocationMfaType::Disabled,
+        location_mfa_mode: LocationMfaMode::Disabled,
     };
     let response = client
         .put(format!("/api/v1/network/{}", network.id))
diff --git a/crates/defguard_core/tests/integration/wireguard_network_import.rs b/crates/defguard_core/tests/integration/wireguard_network_import.rs
index 61cd17b52a..bef4ba9727 100644
--- a/crates/defguard_core/tests/integration/wireguard_network_import.rs
+++ b/crates/defguard_core/tests/integration/wireguard_network_import.rs
@@ -6,7 +6,7 @@ use defguard_core::{
         models::{
             device::{DeviceType, UserDevice},
             wireguard::{
-                DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaType,
+                DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaMode,
             },
         },
     },
@@ -61,7 +61,7 @@ async fn test_config_import(_: PgPoolOptions, options: PgConnectOptions) {
         DEFAULT_DISCONNECT_THRESHOLD,
         false,
         false,
-        LocationMfaType::Disabled,
+        LocationMfaMode::Disabled,
     )
     .unwrap();
     initial_network.save(&pool).await.unwrap();
diff --git a/migrations/20250714203243_add_location_mfa_settings.down.sql b/migrations/20250714203243_add_location_mfa_settings.down.sql
index dab5e81e4e..f869736c67 100644
--- a/migrations/20250714203243_add_location_mfa_settings.down.sql
+++ b/migrations/20250714203243_add_location_mfa_settings.down.sql
@@ -4,7 +4,7 @@ ALTER TABLE wireguard_network ADD COLUMN "mfa_enabled" BOOLEAN DEFAULT false;
 -- populate based on MFA type
 UPDATE wireguard_network
 SET mfa_enabled = CASE
-    WHEN location_mfa = 'disabled'::location_mfa_type THEN false
+    WHEN location_mfa_mode = 'disabled'::location_mfa_mode THEN false
     ELSE true
 END;
 --
@@ -12,8 +12,8 @@ END;
 ALTER TABLE wireguard_network ALTER COLUMN "mfa_enabled" SET NOT NULL;
 
 -- drop new column and type
-ALTER TABLE wireguard_network DROP COLUMN "location_mfa";
-DROP TYPE location_mfa_type;
+ALTER TABLE wireguard_network DROP COLUMN "location_mfa_mode";
+DROP TYPE location_mfa_mode;
 
 -- restore `use_openid_for_mfa` setting
 ALTER TABLE settings ADD COLUMN use_openid_for_mfa BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/migrations/20250714203243_add_location_mfa_settings.up.sql b/migrations/20250714203243_add_location_mfa_settings.up.sql
index fac4906f23..28dda93113 100644
--- a/migrations/20250714203243_add_location_mfa_settings.up.sql
+++ b/migrations/20250714203243_add_location_mfa_settings.up.sql
@@ -1,23 +1,23 @@
 -- add enum representing location MFA configuration
-CREATE TYPE location_mfa_type AS ENUM (
+CREATE TYPE location_mfa_mode AS ENUM (
     'disabled',
     'internal',
     'external'
 );
 
 -- add nullable column to `wireguard_network` table
-ALTER TABLE wireguard_network ADD COLUMN "location_mfa" location_mfa_type DEFAULT 'disabled';
+ALTER TABLE wireguard_network ADD COLUMN "location_mfa_mode" location_mfa_mode DEFAULT 'disabled';
 
 -- populate new column based on value in `mfa_enabled` column
 -- previously only internal MFA was available
 UPDATE wireguard_network
-SET location_mfa = CASE
-    WHEN mfa_enabled = true THEN 'internal'::location_mfa_type
-    ELSE 'disabled'::location_mfa_type
+SET location_mfa_mode = CASE
+    WHEN mfa_enabled = true THEN 'internal'::location_mfa_mode
+    ELSE 'disabled'::location_mfa_mode
 END;
 
 -- make new column NOT NULL
-ALTER TABLE wireguard_network ALTER COLUMN "location_mfa" SET NOT NULL;
+ALTER TABLE wireguard_network ALTER COLUMN "location_mfa_mode" SET NOT NULL;
 
 -- drop the `mfa_enabled` column since it's no longer needed
 ALTER TABLE wireguard_network DROP COLUMN mfa_enabled;
diff --git a/proto b/proto
index 8dcc9d83a8..b9f24ac413 160000
--- a/proto
+++ b/proto
@@ -1 +1 @@
-Subproject commit 8dcc9d83a8cd83a2f8e1852be5d47f1fe233f3a8
+Subproject commit b9f24ac41326bffe4c7e72019ccc8a785f6bd343
diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts
index 7106a22beb..ddd2ffd72d 100644
--- a/web/src/i18n/en/index.ts
+++ b/web/src/i18n/en/index.ts
@@ -1130,7 +1130,7 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
         contact: 'by contacting:',
       },
     },
-    locationMfaTypeSelect: {
+    locationMfaModeSelect: {
       label: 'MFA Requirement',
       options: {
         disabled: 'Do not enforce MFA',
diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts
index 173a0e55a5..a95816a235 100644
--- a/web/src/i18n/i18n-types.ts
+++ b/web/src/i18n/i18n-types.ts
@@ -2779,7 +2779,7 @@ type RootTranslation = {
 				contact: string
 			}
 		}
-		locationMfaTypeSelect: {
+		locationMfaModeSelect: {
 			/**
 			 * M​F​A​ ​R​e​q​u​i​r​e​m​e​n​t
 			 */
@@ -9355,7 +9355,7 @@ export type TranslationFunctions = {
 				contact: () => LocalizedString
 			}
 		}
-		locationMfaTypeSelect: {
+		locationMfaModeSelect: {
 			/**
 			 * MFA Requirement
 			 */
diff --git a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
index 7b268ec9e3..3e8a2994e4 100644
--- a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
+++ b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
@@ -11,7 +11,7 @@ import { shallow } from 'zustand/shallow';
 
 import { useI18nContext } from '../../../i18n/i18n-react';
 import { FormAclDefaultPolicy } from '../../../shared/components/Form/FormAclDefaultPolicySelect/FormAclDefaultPolicy.tsx';
-import { FormLocationMfaTypeSelect } from '../../../shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx';
+import { FormLocationMfaModeSelect } from '../../../shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx';
 import { FormCheckBox } from '../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx';
 import { FormInput } from '../../../shared/defguard-ui/components/Form/FormInput/FormInput';
 import { FormSelect } from '../../../shared/defguard-ui/components/Form/FormSelect/FormSelect';
@@ -22,7 +22,7 @@ import { useAppStore } from '../../../shared/hooks/store/useAppStore.ts';
 import useApi from '../../../shared/hooks/useApi';
 import { useToaster } from '../../../shared/hooks/useToaster';
 import { QueryKeys } from '../../../shared/queries';
-import { LocationMfaType, type Network } from '../../../shared/types';
+import { LocationMfaMode, type Network } from '../../../shared/types';
 import { titleCase } from '../../../shared/utils/titleCase';
 import { trimObjectStrings } from '../../../shared/utils/trimObjectStrings.ts';
 import {
@@ -155,7 +155,7 @@ export const NetworkEditForm = () => {
           .min(120, LL.form.error.invalid()),
         acl_enabled: z.boolean(),
         acl_default_allow: z.boolean(),
-        location_mfa: z.nativeEnum(LocationMfaType),
+        location_mfa_mode: z.nativeEnum(LocationMfaMode),
       }),
     [LL.form.error],
   );
@@ -175,7 +175,7 @@ export const NetworkEditForm = () => {
       peer_disconnect_threshold: 180,
       acl_enabled: false,
       acl_default_allow: false,
-      location_mfa: LocationMfaType.DISABLED,
+      location_mfa_mode: LocationMfaMode.DISABLED,
     }),
     [],
   );
@@ -341,7 +341,7 @@ export const NetworkEditForm = () => {
           label={LL.networkConfiguration.form.fields.peer_disconnect_threshold.label()}
           type="number"
         />
-        <FormLocationMfaTypeSelect controller={{ control, name: 'location_mfa' }} />
+        <FormLocationMfaModeSelect controller={{ control, name: 'location_mfa_mode' }} />
         <button type="submit" className="hidden" ref={submitRef}></button>
       </form>
     </section>
diff --git a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx
index 05fa368f81..48150a6f4f 100644
--- a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx
+++ b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx
@@ -9,7 +9,7 @@ import { shallow } from 'zustand/shallow';
 
 import { useI18nContext } from '../../../../i18n/i18n-react';
 import { FormAclDefaultPolicy } from '../../../../shared/components/Form/FormAclDefaultPolicySelect/FormAclDefaultPolicy.tsx';
-import { FormLocationMfaTypeSelect } from '../../../../shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx';
+import { FormLocationMfaModeSelect } from '../../../../shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx';
 import { FormCheckBox } from '../../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx';
 import { FormInput } from '../../../../shared/defguard-ui/components/Form/FormInput/FormInput';
 import { FormSelect } from '../../../../shared/defguard-ui/components/Form/FormSelect/FormSelect';
@@ -19,7 +19,7 @@ import type { SelectOption } from '../../../../shared/defguard-ui/components/Lay
 import useApi from '../../../../shared/hooks/useApi';
 import { useToaster } from '../../../../shared/hooks/useToaster';
 import { QueryKeys } from '../../../../shared/queries';
-import { LocationMfaType } from '../../../../shared/types.ts';
+import { LocationMfaMode } from '../../../../shared/types.ts';
 import { titleCase } from '../../../../shared/utils/titleCase';
 import { trimObjectStrings } from '../../../../shared/utils/trimObjectStrings.ts';
 import { validateIpList, validateIpOrDomainList } from '../../../../shared/validators';
@@ -132,7 +132,7 @@ export const WizardNetworkConfiguration = () => {
           .refine((v) => v >= 120, LL.form.error.minimumLength()),
         acl_enabled: z.boolean(),
         acl_default_allow: z.boolean(),
-        location_mfa: z.nativeEnum(LocationMfaType),
+        location_mfa_mode: z.nativeEnum(LocationMfaMode),
       }),
     [LL.form.error],
   );
@@ -239,7 +239,7 @@ export const WizardNetworkConfiguration = () => {
           label={LL.networkConfiguration.form.fields.peer_disconnect_threshold.label()}
           type="number"
         />
-        <FormLocationMfaTypeSelect controller={{ control, name: 'location_mfa' }} />
+        <FormLocationMfaModeSelect controller={{ control, name: 'location_mfa_mode' }} />
         <input type="submit" className="visually-hidden" ref={submitRef} />
       </form>
     </Card>
diff --git a/web/src/pages/wizard/hooks/useWizardStore.ts b/web/src/pages/wizard/hooks/useWizardStore.ts
index 6102d8cee0..2404349881 100644
--- a/web/src/pages/wizard/hooks/useWizardStore.ts
+++ b/web/src/pages/wizard/hooks/useWizardStore.ts
@@ -5,7 +5,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
 
 import {
   type ImportedDevice,
-  LocationMfaType,
+  LocationMfaMode,
   type Network,
 } from '../../../shared/types';
 
@@ -33,7 +33,7 @@ const defaultValues: StoreFields = {
     peer_disconnect_threshold: 180,
     acl_enabled: false,
     acl_default_allow: false,
-    location_mfa: LocationMfaType.DISABLED,
+    location_mfa_mode: LocationMfaMode.DISABLED,
   },
 };
 
@@ -89,7 +89,7 @@ type StoreFields = {
     peer_disconnect_threshold: number;
     acl_enabled: boolean;
     acl_default_allow: boolean;
-    location_mfa: LocationMfaType;
+    location_mfa_mode: LocationMfaMode;
   };
 };
 
diff --git a/web/src/shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
similarity index 55%
rename from web/src/shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx
rename to web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
index aed2c0910b..c0600c22c4 100644
--- a/web/src/shared/components/Form/FormLocationMfaTypeSelect/FormLocationMfaTypeSelect.tsx
+++ b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
@@ -4,35 +4,35 @@ import type { FieldValues, UseControllerProps } from 'react-hook-form';
 import { useI18nContext } from '../../../../i18n/i18n-react';
 import { FormSelect } from '../../../defguard-ui/components/Form/FormSelect/FormSelect';
 import type { SelectOption } from '../../../defguard-ui/components/Layout/Select/types';
-import { LocationMfaType } from '../../../types';
+import { LocationMfaMode } from '../../../types';
 
 type Props<T extends FieldValues> = {
   controller: UseControllerProps<T>;
   disabled?: boolean;
 };
 
-export const FormLocationMfaTypeSelect = <T extends FieldValues>({
+export const FormLocationMfaModeSelect = <T extends FieldValues>({
   controller,
   disabled = false,
 }: Props<T>) => {
   const { LL } = useI18nContext();
 
   const options = useMemo(
-    (): SelectOption<LocationMfaType>[] => [
+    (): SelectOption<LocationMfaMode>[] => [
       {
-        key: LocationMfaType.DISABLED,
-        value: LocationMfaType.DISABLED,
-        label: LL.components.locationMfaTypeSelect.options.disabled(),
+        key: LocationMfaMode.DISABLED,
+        value: LocationMfaMode.DISABLED,
+        label: LL.components.locationMfaModeSelect.options.disabled(),
       },
       {
-        key: LocationMfaType.INTERNAL,
-        value: LocationMfaType.INTERNAL,
-        label: LL.components.locationMfaTypeSelect.options.internal(),
+        key: LocationMfaMode.INTERNAL,
+        value: LocationMfaMode.INTERNAL,
+        label: LL.components.locationMfaModeSelect.options.internal(),
       },
       {
-        key: LocationMfaType.EXTERNAL,
-        value: LocationMfaType.EXTERNAL,
-        label: LL.components.locationMfaTypeSelect.options.external(),
+        key: LocationMfaMode.EXTERNAL,
+        value: LocationMfaMode.EXTERNAL,
+        label: LL.components.locationMfaModeSelect.options.external(),
       },
     ],
     [LL.components.aclDefaultPolicySelect.options],
@@ -41,7 +41,7 @@ export const FormLocationMfaTypeSelect = <T extends FieldValues>({
     <FormSelect
       controller={controller}
       options={options}
-      label={LL.components.locationMfaTypeSelect.label()}
+      label={LL.components.locationMfaModeSelect.label()}
       disabled={disabled}
     />
   );
diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts
index 50e609f85b..8d69148ad7 100644
--- a/web/src/shared/types.ts
+++ b/web/src/shared/types.ts
@@ -118,7 +118,7 @@ export type GatewayStatus = {
   uid: string;
 };
 
-export enum LocationMfaType {
+export enum LocationMfaMode {
   DISABLED = 'disabled',
   INTERNAL = 'internal',
   EXTERNAL = 'external',
@@ -140,7 +140,7 @@ export interface Network {
   peer_disconnect_threshold: number;
   acl_enabled: boolean;
   acl_default_allow: boolean;
-  location_mfa: LocationMfaType;
+  location_mfa_mode: LocationMfaMode;
 }
 
 export type ModifyNetworkRequest = {

From b37e0b94ea8efe14ff00b613ec711cc9fed1830d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Thu, 17 Jul 2025 14:08:22 +0200
Subject: [PATCH 14/26] handle removal of openid provider

---
 .../defguard_core/src/db/models/wireguard.rs  | 20 ++++++++++++++++++
 .../enterprise/handlers/openid_providers.rs   | 21 ++++++++++++++++---
 2 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs
index 4719d15e4d..b169149c88 100644
--- a/crates/defguard_core/src/db/models/wireguard.rs
+++ b/crates/defguard_core/src/db/models/wireguard.rs
@@ -1269,6 +1269,26 @@ impl WireguardNetwork<Id> {
             LocationMfaMode::Disabled => false,
         }
     }
+
+    // fetch all locations using external MFA
+    pub(crate) async fn all_using_external_mfa<'e, E>(
+        executor: E,
+    ) -> Result<Vec<Self>, WireguardNetworkError>
+    where
+        E: PgExecutor<'e>,
+    {
+        let locations = query_as!(
+            WireguardNetwork,
+            "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \
+            connected_at, keepalive_interval, peer_disconnect_threshold, \
+            acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \
+            FROM wireguard_network WHERE location_mfa_mode = 'external'::location_mfa_mode",
+        )
+        .fetch_all(executor)
+        .await?;
+
+        Ok(locations)
+    }
 }
 
 // [`IpNetwork`] does not implement [`Default`]
diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
index 97d72f0453..26e537f3b1 100644
--- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
+++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
@@ -11,8 +11,11 @@ use crate::{
     appstate::AppState,
     auth::{AdminRole, SessionInfo},
     db::{
-        Settings,
-        models::settings::{OpenidUsernameHandling, update_current_settings},
+        Settings, WireguardNetwork,
+        models::{
+            settings::{OpenidUsernameHandling, update_current_settings},
+            wireguard::LocationMfaMode,
+        },
     },
     enterprise::{
         db::models::openid_provider::OpenIdProvider, directory_sync::test_directory_sync_connection,
@@ -216,7 +219,19 @@ pub async fn delete_openid_provider(
     let provider = OpenIdProvider::find_by_name(&mut *transaction, &provider_data.name).await?;
     if let Some(provider) = provider {
         provider.clone().delete(&mut *transaction).await?;
-        // FIXME: update locations using external MFA
+        // fetch all locations using external MFA
+        let locations = WireguardNetwork::all_using_external_mfa(&mut *transaction).await?;
+        if locations.is_empty() {
+            debug!("No locations are using OIDC provider for external MFA");
+        };
+        // fall back to internal MFA in all relevant locations
+        for mut location in locations {
+            debug!(
+                "Falling back to internal MFA for {location} because exteral OIDC provider has been removed"
+            );
+            location.location_mfa_mode = LocationMfaMode::Internal;
+            location.save(&mut *transaction).await?;
+        }
         transaction.commit().await?;
         info!(
             "User {} deleted OpenID provider {}",

From 8ae8dfcf898669ed0c6391bdb0dc124aeafef562 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Thu, 17 Jul 2025 14:12:04 +0200
Subject: [PATCH 15/26] update query data

---
 ...2527807bd9643f2758c83c728d1d003843ab.json} |   8 +-
 ...375d5674fd59fe3018af120ae2ef5fd10f48.json} |   8 +-
 ...eadc2d27eeb56d18f9debf50d0ba71e01e48.json} |   8 +-
 ...a332453a9626e56a5f543c2790bff9d2109c.json} |   8 +-
 ...10e5106af580e45647ae620850de0b77785b.json} |   8 +-
 ...e2bf4a03081f33c0b4e779e5e431b092fbdc.json} |   8 +-
 ...44149463f9a39f673b1f83587980c527c575.json} |   8 +-
 ...7e1aea7304e054c1241406aa62993d66117a.json} |   6 +-
 ...50697d20952973b676d574b01d0836f1aff0.json} |   6 +-
 ...f2f325d5ec81e6e0ea10660f74084718ac48e.json | 115 ++++++++++++++++++
 10 files changed, 149 insertions(+), 34 deletions(-)
 rename .sqlx/{query-4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1.json => query-0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab.json} (90%)
 rename .sqlx/{query-429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d.json => query-0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48.json} (85%)
 rename .sqlx/{query-12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json => query-21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48.json} (89%)
 rename .sqlx/{query-133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b.json => query-32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c.json} (90%)
 rename .sqlx/{query-fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54.json => query-5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b.json} (87%)
 rename .sqlx/{query-0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789.json => query-6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc.json} (89%)
 rename .sqlx/{query-317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef.json => query-89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575.json} (87%)
 rename .sqlx/{query-a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7.json => query-966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a.json} (82%)
 rename .sqlx/{query-8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1.json => query-acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0.json} (85%)
 create mode 100644 .sqlx/query-d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e.json

diff --git a/.sqlx/query-4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1.json b/.sqlx/query-0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab.json
similarity index 90%
rename from .sqlx/query-4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1.json
rename to .sqlx/query-0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab.json
index 0d28b62520..da1e780cd7 100644
--- a/.sqlx/query-4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1.json
+++ b/.sqlx/query-0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa\" \"location_mfa: _\" FROM \"wireguard_network\"",
+  "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\" \"location_mfa_mode: _\" FROM \"wireguard_network\"",
   "describe": {
     "columns": [
       {
@@ -75,10 +75,10 @@
       },
       {
         "ordinal": 14,
-        "name": "location_mfa: _",
+        "name": "location_mfa_mode: _",
         "type_info": {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -111,5 +111,5 @@
       false
     ]
   },
-  "hash": "4c2bac7f884c1dd477a48a51e60be04120ebdfc78c0dfac15b15781a85a212c1"
+  "hash": "0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab"
 }
diff --git a/.sqlx/query-429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d.json b/.sqlx/query-0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48.json
similarity index 85%
rename from .sqlx/query-429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d.json
rename to .sqlx/query-0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48.json
index 4746c54236..4d94977db3 100644
--- a/.sqlx/query-429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d.json
+++ b/.sqlx/query-0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at,  keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM wireguard_network WHERE id IN (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at,  keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE id IN (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)",
   "describe": {
     "columns": [
       {
@@ -75,10 +75,10 @@
       },
       {
         "ordinal": 14,
-        "name": "location_mfa: LocationMfaType",
+        "name": "location_mfa_mode: LocationMfaMode",
         "type_info": {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -113,5 +113,5 @@
       false
     ]
   },
-  "hash": "429e9c277b1a66daea1569da2c9beb4518c5dd109f30d620b169ae6f3d751a0d"
+  "hash": "0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48"
 }
diff --git a/.sqlx/query-12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json b/.sqlx/query-21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48.json
similarity index 89%
rename from .sqlx/query-12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json
rename to .sqlx/query-21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48.json
index cdd890b40d..1b397c0991 100644
--- a/.sqlx/query-12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b.json
+++ b/.sqlx/query-21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM wireguard_network WHERE id = $1",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE id = $1",
   "describe": {
     "columns": [
       {
@@ -75,10 +75,10 @@
       },
       {
         "ordinal": 14,
-        "name": "location_mfa: LocationMfaType",
+        "name": "location_mfa_mode: LocationMfaMode",
         "type_info": {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -113,5 +113,5 @@
       false
     ]
   },
-  "hash": "12fc40b3a359a953dfb801e9a1fe1aa1ceb3ecb8b70222f5ca86062b2c0b952b"
+  "hash": "21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48"
 }
diff --git a/.sqlx/query-133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b.json b/.sqlx/query-32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c.json
similarity index 90%
rename from .sqlx/query-133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b.json
rename to .sqlx/query-32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c.json
index 094efeef12..37b1fe0d05 100644
--- a/.sqlx/query-133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b.json
+++ b/.sqlx/query-32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa\" \"location_mfa: _\" FROM \"wireguard_network\" WHERE id = $1",
+  "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\" \"location_mfa_mode: _\" FROM \"wireguard_network\" WHERE id = $1",
   "describe": {
     "columns": [
       {
@@ -75,10 +75,10 @@
       },
       {
         "ordinal": 14,
-        "name": "location_mfa: _",
+        "name": "location_mfa_mode: _",
         "type_info": {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -113,5 +113,5 @@
       false
     ]
   },
-  "hash": "133baa96e578cf2ca3e551b3d9f3e1130676812d3c5ed48cffe347e59d930b9b"
+  "hash": "32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c"
 }
diff --git a/.sqlx/query-fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54.json b/.sqlx/query-5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b.json
similarity index 87%
rename from .sqlx/query-fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54.json
rename to .sqlx/query-5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b.json
index 7b06fae1e2..36d70341c3 100644
--- a/.sqlx/query-fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54.json
+++ b/.sqlx/query-5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM aclrulenetwork r JOIN wireguard_network n ON n.id = r.network_id WHERE r.rule_id = $1",
+  "query": "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM aclrulenetwork r JOIN wireguard_network n ON n.id = r.network_id WHERE r.rule_id = $1",
   "describe": {
     "columns": [
       {
@@ -75,10 +75,10 @@
       },
       {
         "ordinal": 14,
-        "name": "location_mfa: LocationMfaType",
+        "name": "location_mfa_mode: LocationMfaMode",
         "type_info": {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -113,5 +113,5 @@
       false
     ]
   },
-  "hash": "fcc34fc3baf2016f3f9adfd13060290465bf658471b97bc06ec386ff0a976b54"
+  "hash": "5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b"
 }
diff --git a/.sqlx/query-0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789.json b/.sqlx/query-6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc.json
similarity index 89%
rename from .sqlx/query-0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789.json
rename to .sqlx/query-6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc.json
index 8a61dbe7c5..4b43522b73 100644
--- a/.sqlx/query-0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789.json
+++ b/.sqlx/query-6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM wireguard_network WHERE name = $1",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE name = $1",
   "describe": {
     "columns": [
       {
@@ -75,10 +75,10 @@
       },
       {
         "ordinal": 14,
-        "name": "location_mfa: LocationMfaType",
+        "name": "location_mfa_mode: LocationMfaMode",
         "type_info": {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -113,5 +113,5 @@
       false
     ]
   },
-  "hash": "0c40a810ececc9d36a225e28a9f02e43982729517479cecc61bdd9bf046da789"
+  "hash": "6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc"
 }
diff --git a/.sqlx/query-317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef.json b/.sqlx/query-89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575.json
similarity index 87%
rename from .sqlx/query-317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef.json
rename to .sqlx/query-89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575.json
index 9b6ef47f1f..55255fc63f 100644
--- a/.sqlx/query-317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef.json
+++ b/.sqlx/query-89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa \"location_mfa: LocationMfaType\" FROM wireguard_network WHERE location_mfa != 'disabled'::location_mfa_type",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE location_mfa_mode = 'external'::location_mfa_mode",
   "describe": {
     "columns": [
       {
@@ -75,10 +75,10 @@
       },
       {
         "ordinal": 14,
-        "name": "location_mfa: LocationMfaType",
+        "name": "location_mfa_mode: LocationMfaMode",
         "type_info": {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -111,5 +111,5 @@
       false
     ]
   },
-  "hash": "317f3bca456c72f2be625b9b50a5a801fb6b9c61b20240ab9f365473373756ef"
+  "hash": "89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575"
 }
diff --git a/.sqlx/query-a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7.json b/.sqlx/query-966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a.json
similarity index 82%
rename from .sqlx/query-a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7.json
rename to .sqlx/query-966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a.json
index 8d902bef66..070daed9c5 100644
--- a/.sqlx/query-a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7.json
+++ b/.sqlx/query-966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "INSERT INTO \"wireguard_network\" (\"name\",\"address\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id",
+  "query": "INSERT INTO \"wireguard_network\" (\"name\",\"address\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id",
   "describe": {
     "columns": [
       {
@@ -26,7 +26,7 @@
         "Int4",
         {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -42,5 +42,5 @@
       false
     ]
   },
-  "hash": "a96a1e9cb7707403db409a40137264a3955d56ee1930763d2ed372fd4123e0f7"
+  "hash": "966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a"
 }
diff --git a/.sqlx/query-8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1.json b/.sqlx/query-acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0.json
similarity index 85%
rename from .sqlx/query-8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1.json
rename to .sqlx/query-acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0.json
index 73802a08e9..933ffa8b91 100644
--- a/.sqlx/query-8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1.json
+++ b/.sqlx/query-acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "UPDATE \"wireguard_network\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"pubkey\" = $5,\"prvkey\" = $6,\"endpoint\" = $7,\"dns\" = $8,\"allowed_ips\" = $9,\"connected_at\" = $10,\"acl_enabled\" = $11,\"acl_default_allow\" = $12,\"keepalive_interval\" = $13,\"peer_disconnect_threshold\" = $14,\"location_mfa\" = $15 WHERE id = $1",
+  "query": "UPDATE \"wireguard_network\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"pubkey\" = $5,\"prvkey\" = $6,\"endpoint\" = $7,\"dns\" = $8,\"allowed_ips\" = $9,\"connected_at\" = $10,\"acl_enabled\" = $11,\"acl_default_allow\" = $12,\"keepalive_interval\" = $13,\"peer_disconnect_threshold\" = $14,\"location_mfa_mode\" = $15 WHERE id = $1",
   "describe": {
     "columns": [],
     "parameters": {
@@ -21,7 +21,7 @@
         "Int4",
         {
           "Custom": {
-            "name": "location_mfa_type",
+            "name": "location_mfa_mode",
             "kind": {
               "Enum": [
                 "disabled",
@@ -35,5 +35,5 @@
     },
     "nullable": []
   },
-  "hash": "8f1e5d84d5b6d7789aeaba6ba4d27fe3b88446fba18f94b199e97176adca61d1"
+  "hash": "acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0"
 }
diff --git a/.sqlx/query-d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e.json b/.sqlx/query-d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e.json
new file mode 100644
index 0000000000..cee5190d9a
--- /dev/null
+++ b/.sqlx/query-d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e.json
@@ -0,0 +1,115 @@
+{
+  "db_name": "PostgreSQL",
+  "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE location_mfa_mode != 'disabled'::location_mfa_mode",
+  "describe": {
+    "columns": [
+      {
+        "ordinal": 0,
+        "name": "id",
+        "type_info": "Int8"
+      },
+      {
+        "ordinal": 1,
+        "name": "name",
+        "type_info": "Text"
+      },
+      {
+        "ordinal": 2,
+        "name": "address",
+        "type_info": "InetArray"
+      },
+      {
+        "ordinal": 3,
+        "name": "port",
+        "type_info": "Int4"
+      },
+      {
+        "ordinal": 4,
+        "name": "pubkey",
+        "type_info": "Text"
+      },
+      {
+        "ordinal": 5,
+        "name": "prvkey",
+        "type_info": "Text"
+      },
+      {
+        "ordinal": 6,
+        "name": "endpoint",
+        "type_info": "Text"
+      },
+      {
+        "ordinal": 7,
+        "name": "dns",
+        "type_info": "Text"
+      },
+      {
+        "ordinal": 8,
+        "name": "allowed_ips",
+        "type_info": "InetArray"
+      },
+      {
+        "ordinal": 9,
+        "name": "connected_at",
+        "type_info": "Timestamp"
+      },
+      {
+        "ordinal": 10,
+        "name": "keepalive_interval",
+        "type_info": "Int4"
+      },
+      {
+        "ordinal": 11,
+        "name": "peer_disconnect_threshold",
+        "type_info": "Int4"
+      },
+      {
+        "ordinal": 12,
+        "name": "acl_enabled",
+        "type_info": "Bool"
+      },
+      {
+        "ordinal": 13,
+        "name": "acl_default_allow",
+        "type_info": "Bool"
+      },
+      {
+        "ordinal": 14,
+        "name": "location_mfa_mode: LocationMfaMode",
+        "type_info": {
+          "Custom": {
+            "name": "location_mfa_mode",
+            "kind": {
+              "Enum": [
+                "disabled",
+                "internal",
+                "external"
+              ]
+            }
+          }
+        }
+      }
+    ],
+    "parameters": {
+      "Left": []
+    },
+    "nullable": [
+      false,
+      false,
+      false,
+      false,
+      false,
+      false,
+      false,
+      true,
+      false,
+      true,
+      false,
+      false,
+      false,
+      false,
+      false
+    ]
+  },
+  "hash": "d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e"
+}

From de5d3cb347a69efeabebe7f9a49a91422b5fb2b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Thu, 17 Jul 2025 14:14:13 +0200
Subject: [PATCH 16/26] bump version to 1.5.0

---
 Cargo.lock                      | 4 ++--
 crates/defguard/Cargo.toml      | 2 +-
 crates/defguard_core/Cargo.toml | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index dc30ecc2e2..bb120059e9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1059,7 +1059,7 @@ dependencies = [
 
 [[package]]
 name = "defguard"
-version = "1.4.0"
+version = "1.5.0"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1075,7 +1075,7 @@ dependencies = [
 
 [[package]]
 name = "defguard_core"
-version = "1.4.0"
+version = "1.5.0"
 dependencies = [
  "anyhow",
  "argon2",
diff --git a/crates/defguard/Cargo.toml b/crates/defguard/Cargo.toml
index 443dbea9f3..5c453e15fe 100644
--- a/crates/defguard/Cargo.toml
+++ b/crates/defguard/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "defguard"
-version = "1.4.0"
+version = "1.5.0"
 edition.workspace = true
 license-file.workspace = true
 homepage.workspace = true
diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml
index 99d15ed573..0e07be4195 100644
--- a/crates/defguard_core/Cargo.toml
+++ b/crates/defguard_core/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "defguard_core"
-version = "1.4.0"
+version = "1.5.0"
 edition.workspace = true
 license-file.workspace = true
 homepage.workspace = true

From 8878c9eb958b94f36edfb57fa7bb8e3cde490675 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Fri, 18 Jul 2025 10:13:17 +0200
Subject: [PATCH 17/26] validate correct MFA method is selected

---
 crates/defguard_core/src/db/models/user.rs    |   2 +-
 .../defguard_core/src/db/models/wireguard.rs  |  12 +-
 .../src/grpc/desktop_client_mfa.rs            | 106 +++++++++++++-----
 3 files changed, 88 insertions(+), 32 deletions(-)

diff --git a/crates/defguard_core/src/db/models/user.rs b/crates/defguard_core/src/db/models/user.rs
index 37dbc38044..80df0cb658 100644
--- a/crates/defguard_core/src/db/models/user.rs
+++ b/crates/defguard_core/src/db/models/user.rs
@@ -377,7 +377,7 @@ impl User<Id> {
         .await
     }
 
-    /// Verify the state of mfa flags are correct.
+    /// Verify the state of MFA flags are correct.
     /// Recovers from invalid mfa_method
     /// Use this function after removing any of the authentication factors.
     pub async fn verify_mfa_state(&mut self, pool: &PgPool) -> Result<(), WebError> {
diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs
index b169149c88..5fa1fd84dc 100644
--- a/crates/defguard_core/src/db/models/wireguard.rs
+++ b/crates/defguard_core/src/db/models/wireguard.rs
@@ -1,6 +1,6 @@
 use std::{
     collections::HashMap,
-    fmt,
+    fmt::{self, Display},
     iter::zip,
     net::{IpAddr, Ipv4Addr},
 };
@@ -95,6 +95,16 @@ pub enum LocationMfaMode {
     External,
 }
 
+impl Display for LocationMfaMode {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            LocationMfaMode::Disabled => write!(f, "MFA disabled"),
+            LocationMfaMode::Internal => write!(f, "Internal MFA"),
+            LocationMfaMode::External => write!(f, "External MFA"),
+        }
+    }
+}
+
 impl From<ProtoLocationMfaMode> for LocationMfaMode {
     fn from(value: ProtoLocationMfaMode) -> Self {
         match value {
diff --git a/crates/defguard_core/src/grpc/desktop_client_mfa.rs b/crates/defguard_core/src/grpc/desktop_client_mfa.rs
index 3dabde6671..41cef0d866 100644
--- a/crates/defguard_core/src/grpc/desktop_client_mfa.rs
+++ b/crates/defguard_core/src/grpc/desktop_client_mfa.rs
@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 
 use chrono::Utc;
-use sqlx::PgPool;
+use sqlx::{PgConnection, PgPool};
 use thiserror::Error;
 use tokio::sync::{
     broadcast::Sender,
@@ -17,7 +17,10 @@ use crate::{
     auth::{Claims, ClaimsType},
     db::{
         Device, GatewayEvent, Id, User, UserInfo, WireguardNetwork,
-        models::device::{DeviceInfo, DeviceNetworkInfo, WireguardNetworkDevice},
+        models::{
+            device::{DeviceInfo, DeviceNetworkInfo, WireguardNetworkDevice},
+            wireguard::LocationMfaMode,
+        },
     },
     enterprise::{db::models::openid_provider::OpenIdProvider, is_enterprise_enabled},
     events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent},
@@ -115,6 +118,12 @@ impl ClientMfaServer {
             return Err(Status::invalid_argument("location not found"));
         };
 
+        // return early if MFA is not enabled for this location
+        if !location.mfa_enabled() {
+            error!("MFA is not enabled for location {location}");
+            return Err(Status::invalid_argument("MFA not enabled for location"));
+        }
+
         // fetch device
         let Ok(Some(device)) = Device::find_by_pubkey(&self.pool, &request.pubkey).await else {
             error!("Failed to find device with pubkey {}", request.pubkey);
@@ -131,32 +140,14 @@ impl ClientMfaServer {
             Status::internal("unexpected error")
         })?;
 
-        // validate user is allowed to connect to a given location
+        // begin transaction
         let mut transaction = self.pool.begin().await.map_err(|_| {
             error!("Failed to begin transaction");
             Status::internal("unexpected error")
         })?;
-        let allowed_groups = location
-            .get_allowed_groups(&mut transaction)
-            .await
-            .map_err(|err| {
-                error!("Failed to fetch allowed groups for location {location}: {err:?}");
-                Status::internal("unexpected error")
-            })?;
-        if let Some(groups) = allowed_groups {
-            // check if user belongs to one of allowed groups
-            if !groups
-                .iter()
-                .any(|allowed_group| user_info.groups.contains(allowed_group))
-            {
-                error!(
-                    "User {} not allowed to connect to location {location} because he doesn't belong to any of the allowed groups.
-                    User groups: {:?}, allowed groups: {:?}",
-                    user.username, user_info.groups, groups
-                );
-                return Err(Status::unauthenticated("unauthorized"));
-            }
-        }
+
+        // validate user is allowed to connect to a given location
+        Self::validate_location_access(&mut *transaction, &location, &user_info).await?;
 
         user.verify_mfa_state(&self.pool).await.map_err(|err| {
             error!(
@@ -166,14 +157,37 @@ impl ClientMfaServer {
             Status::internal("unexpected error")
         })?;
 
-        // FIXME: check which method is enabled for this location
-
-        // check if selected method is enabled
-        let method = MfaMethod::try_from(request.method).map_err(|err| {
+        // extract user selected method from request
+        let selected_method = MfaMethod::try_from(request.method).map_err(|err| {
             error!("Invalid MFA method selected ({}): {err}", request.method);
             Status::invalid_argument("invalid MFA method selected")
         })?;
-        match method {
+
+        // check if selected MFA method matches location settings
+        match (&location.location_mfa_mode, selected_method) {
+            // MFA enabled status is already verified
+            (LocationMfaMode::Disabled, _) => unreachable!(),
+            (LocationMfaMode::Internal, MfaMethod::Totp)
+            | (LocationMfaMode::Internal, MfaMethod::Email) => {
+                debug!("Location uses internal MFA. Selected method: {selected_method}")
+            }
+            (LocationMfaMode::External, MfaMethod::Oidc) => {
+                debug!("Location uses external MFA. Selected method: {selected_method}")
+            }
+            _ => {
+                error!(
+                    "Selected MFA method ({selected_method}) is not supported by location {location} which uses {}",
+                    location.location_mfa_mode
+                );
+
+                return Err(Status::invalid_argument(
+                    "selected MFA method not supported by location",
+                ));
+            }
+        }
+
+        // check if selected method is configured
+        match selected_method {
             MfaMethod::Totp => {
                 if !user.totp_enabled {
                     error!("TOTP not enabled for user {}", user.username);
@@ -234,7 +248,7 @@ impl ClientMfaServer {
         self.sessions.insert(
             request.pubkey,
             ClientLoginSession {
-                method,
+                method: selected_method,
                 location,
                 device,
                 user,
@@ -245,6 +259,38 @@ impl ClientMfaServer {
         Ok(ClientMfaStartResponse { token })
     }
 
+    /// Checks if given user is allowed to access a location
+    async fn validate_location_access(
+        conn: &mut PgConnection,
+        location: &WireguardNetwork<Id>,
+        user_info: &UserInfo,
+    ) -> Result<(), Status> {
+        // fetch allowed group names for a given location
+        let allowed_groups = location
+            .get_allowed_groups(&mut *conn)
+            .await
+            .map_err(|err| {
+                error!("Failed to fetch allowed groups for location {location}: {err:?}");
+                Status::internal("unexpected error")
+            })?;
+        // if no groups are specified all users are allowed
+        if let Some(groups) = allowed_groups {
+            // check if user belongs to one of allowed groups
+            if !groups
+                .iter()
+                .any(|allowed_group| user_info.groups.contains(allowed_group))
+            {
+                error!(
+                    "User {} not allowed to connect to location {location} because he doesn't belong to any of the allowed groups.
+                    User groups: {:?}, allowed groups: {:?}",
+                    user_info.username, user_info.groups, groups
+                );
+                return Err(Status::unauthenticated("unauthorized"));
+            }
+        }
+        Ok(())
+    }
+
     #[instrument(skip_all)]
     pub async fn finish_client_mfa_login(
         &mut self,

From 1c9bcd43c5705a8cd8e008cd8bb80f37213c8576 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Fri, 18 Jul 2025 10:25:45 +0200
Subject: [PATCH 18/26] remove unused transaction

---
 crates/defguard_core/src/db/models/group.rs   |  7 +++----
 .../src/grpc/desktop_client_mfa.rs            | 20 +++++++++----------
 2 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/crates/defguard_core/src/db/models/group.rs b/crates/defguard_core/src/db/models/group.rs
index f0e50f682e..12c79e2b34 100644
--- a/crates/defguard_core/src/db/models/group.rs
+++ b/crates/defguard_core/src/db/models/group.rs
@@ -184,14 +184,13 @@ impl WireguardNetwork<Id> {
     /// access to networks based on allowed groups.
     pub async fn get_allowed_groups(
         &self,
-        transaction: &mut PgConnection,
+        conn: &mut PgConnection,
     ) -> Result<Option<Vec<String>>, ModelError> {
         debug!("Returning a list of allowed groups for network {self}");
-        let admin_groups =
-            Group::find_by_permission(&mut *transaction, Permission::IsAdmin).await?;
+        let admin_groups = Group::find_by_permission(&mut *conn, Permission::IsAdmin).await?;
 
         // get allowed groups from DB
-        let mut groups = self.fetch_allowed_groups(&mut *transaction).await?;
+        let mut groups = self.fetch_allowed_groups(&mut *conn).await?;
 
         // if no allowed groups are set then all groups are allowed
         if groups.is_empty() {
diff --git a/crates/defguard_core/src/grpc/desktop_client_mfa.rs b/crates/defguard_core/src/grpc/desktop_client_mfa.rs
index 41cef0d866..ba8891f433 100644
--- a/crates/defguard_core/src/grpc/desktop_client_mfa.rs
+++ b/crates/defguard_core/src/grpc/desktop_client_mfa.rs
@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 
 use chrono::Utc;
-use sqlx::{PgConnection, PgPool};
+use sqlx::PgPool;
 use thiserror::Error;
 use tokio::sync::{
     broadcast::Sender,
@@ -140,14 +140,8 @@ impl ClientMfaServer {
             Status::internal("unexpected error")
         })?;
 
-        // begin transaction
-        let mut transaction = self.pool.begin().await.map_err(|_| {
-            error!("Failed to begin transaction");
-            Status::internal("unexpected error")
-        })?;
-
         // validate user is allowed to connect to a given location
-        Self::validate_location_access(&mut *transaction, &location, &user_info).await?;
+        Self::validate_location_access(&self.pool, &location, &user_info).await?;
 
         user.verify_mfa_state(&self.pool).await.map_err(|err| {
             error!(
@@ -261,13 +255,19 @@ impl ClientMfaServer {
 
     /// Checks if given user is allowed to access a location
     async fn validate_location_access(
-        conn: &mut PgConnection,
+        pool: &PgPool,
         location: &WireguardNetwork<Id>,
         user_info: &UserInfo,
     ) -> Result<(), Status> {
+        // acquire connection
+        let mut conn = pool.acquire().await.map_err(|_| {
+            error!("Failed to acquire DB connection");
+            Status::internal("unexpected error")
+        })?;
+
         // fetch allowed group names for a given location
         let allowed_groups = location
-            .get_allowed_groups(&mut *conn)
+            .get_allowed_groups(&mut conn)
             .await
             .map_err(|err| {
                 error!("Failed to fetch allowed groups for location {location}: {err:?}");

From 9088ed663d1380ddeaa54c41abd215314ff09da6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Fri, 18 Jul 2025 10:50:12 +0200
Subject: [PATCH 19/26] update test network fixtures

---
 crates/defguard_core/tests/integration/common/mod.rs | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/crates/defguard_core/tests/integration/common/mod.rs b/crates/defguard_core/tests/integration/common/mod.rs
index d4b03c1ca2..15cf2cd27a 100644
--- a/crates/defguard_core/tests/integration/common/mod.rs
+++ b/crates/defguard_core/tests/integration/common/mod.rs
@@ -200,11 +200,11 @@ pub(crate) async fn exceed_enterprise_limits(client: &TestClient) {
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": [],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -220,11 +220,11 @@ pub(crate) async fn exceed_enterprise_limits(client: &TestClient) {
                 "allowed_ips": "10.1.1.0/24",
                 "dns": "1.1.1.1",
                 "allowed_groups": [],
-                "mfa_enabled": false,
                 "keepalive_interval": 25,
                 "peer_disconnect_threshold": 180,
                 "acl_enabled": false,
-                "acl_default_allow": false
+                "acl_default_allow": false,
+                "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -240,11 +240,11 @@ pub(crate) fn make_network() -> Value {
         "allowed_ips": "10.1.1.0/24",
         "dns": "1.1.1.1",
         "allowed_groups": [],
-        "mfa_enabled": false,
         "keepalive_interval": 25,
         "peer_disconnect_threshold": 180,
         "acl_enabled": false,
-        "acl_default_allow": false
+        "acl_default_allow": false,
+        "location_mfa_mode": "disabled"
     })
 }
 

From 1d8e45215d90233277194362b13eef118ceccf4e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Fri, 18 Jul 2025 11:07:16 +0200
Subject: [PATCH 20/26] remove remaining references to mfa_enabled

---
 crates/defguard_core/src/grpc/enrollment.rs   |  2 +-
 crates/defguard_core/src/grpc/utils.rs        |  4 +--
 .../defguard_core/src/handlers/wireguard.rs   |  4 +--
 .../tests/integration/wireguard.rs            |  8 ++---
 .../wireguard_network_allowed_groups.rs       | 32 +++++++++----------
 .../integration/wireguard_network_devices.rs  |  8 ++---
 web/src/i18n/en/index.ts                      |  3 --
 web/src/i18n/i18n-types.ts                    | 12 -------
 web/src/i18n/ko/index.ts                      |  3 --
 web/src/shared/types.ts                       |  2 +-
 10 files changed, 30 insertions(+), 48 deletions(-)

diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs
index 5f40b68e29..48a8dcddd5 100644
--- a/crates/defguard_core/src/grpc/enrollment.rs
+++ b/crates/defguard_core/src/grpc/enrollment.rs
@@ -851,7 +851,7 @@ impl InitialUserInfo {
 
 impl From<DeviceConfig> for ProtoDeviceConfig {
     fn from(config: DeviceConfig) -> Self {
-        // used by pre-1.5 clients which don't support external MFA
+        // DEPRECATED(1.5): superseeded by location_mfa_mode
         let mfa_enabled = config.location_mfa_mode == LocationMfaMode::Internal;
         Self {
             network_id: config.network_id,
diff --git a/crates/defguard_core/src/grpc/utils.rs b/crates/defguard_core/src/grpc/utils.rs
index 45ba181913..226abbb117 100644
--- a/crates/defguard_core/src/grpc/utils.rs
+++ b/crates/defguard_core/src/grpc/utils.rs
@@ -119,7 +119,7 @@ pub(crate) async fn build_device_config_response(
                     );
                     Status::internal(format!("unexpected error: {err}"))
                 })?;
-            // used by pre-1.5 clients which don't support external MFA
+            // DEPRECATED(1.5): superseeded by location_mfa_mode
             let mfa_enabled = network.location_mfa_mode == LocationMfaMode::Internal;
             let config = ProtoDeviceConfig {
                 config: Device::create_config(&network, &wireguard_network_device),
@@ -155,7 +155,7 @@ pub(crate) async fn build_device_config_response(
                 );
                 Status::internal(format!("unexpected error: {err}"))
             })?;
-            // used by pre-1.5 clients which don't support external MFA
+            // DEPRECATED(1.5): superseeded by location_mfa_mode
             let mfa_enabled = network.location_mfa_mode == LocationMfaMode::Internal;
             if let Some(wireguard_network_device) = wireguard_network_device {
                 let config = ProtoDeviceConfig {
diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs
index 6e8e0bacda..ab4f3d9852 100644
--- a/crates/defguard_core/src/handlers/wireguard.rs
+++ b/crates/defguard_core/src/handlers/wireguard.rs
@@ -637,8 +637,8 @@ pub struct AddDeviceResult {
                         "allowed_ips": ["0.0.0.0:8000"],
                         "pubkey": "pubkey",
                         "dns": "8.8.8.8",
-                        "mfa_enabled": false,
-                        "keepalive_interval": 5
+                        "keepalive_interval": 5,
+			            "location_mfa_mode": "disabled"
                     }
                 ],
                 "device": {
diff --git a/crates/defguard_core/tests/integration/wireguard.rs b/crates/defguard_core/tests/integration/wireguard.rs
index 1e63c93daa..1001769b62 100644
--- a/crates/defguard_core/tests/integration/wireguard.rs
+++ b/crates/defguard_core/tests/integration/wireguard.rs
@@ -298,11 +298,11 @@ async fn test_network_address_reassignment(_: PgPoolOptions, options: PgConnectO
         "allowed_ips": "10.1.1.0/24",
         "dns": "1.1.1.1",
         "allowed_groups": [],
-        "mfa_enabled": false,
         "keepalive_interval": 25,
         "peer_disconnect_threshold": 180,
         "acl_enabled": false,
-        "acl_default_allow": false
+        "acl_default_allow": false,
+        "location_mfa_mode": "disabled"
     });
     let response = client.post("/api/v1/network").json(&network).send().await;
     assert_eq!(response.status(), StatusCode::CREATED);
@@ -366,11 +366,11 @@ async fn test_network_address_reassignment(_: PgPoolOptions, options: PgConnectO
         "allowed_ips": "10.1.1.0/24",
         "dns": "1.1.1.1",
         "allowed_groups": [],
-        "mfa_enabled": false,
         "keepalive_interval": 25,
         "peer_disconnect_threshold": 180,
         "acl_enabled": false,
-        "acl_default_allow": false
+        "acl_default_allow": false,
+        "location_mfa_mode": "disabled"
     });
     let response = client
         .put(format!("/api/v1/network/{}", network_from_details.id))
diff --git a/crates/defguard_core/tests/integration/wireguard_network_allowed_groups.rs b/crates/defguard_core/tests/integration/wireguard_network_allowed_groups.rs
index 42f8a7d993..24c8e528ef 100644
--- a/crates/defguard_core/tests/integration/wireguard_network_allowed_groups.rs
+++ b/crates/defguard_core/tests/integration/wireguard_network_allowed_groups.rs
@@ -147,11 +147,11 @@ async fn test_create_new_network(_: PgPoolOptions, options: PgConnectOptions) {
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": ["allowed group"],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -193,11 +193,11 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) {
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": [],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -226,11 +226,11 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) {
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": ["allowed group"],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -253,11 +253,11 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) {
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": ["allowed group", "not allowed group"],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -281,11 +281,11 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) {
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": ["not allowed group"],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -308,11 +308,11 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) {
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": [],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -557,11 +557,11 @@ async fn test_modify_user(_: PgPoolOptions, options: PgConnectOptions) {
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": ["allowed group"],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
@@ -656,11 +656,11 @@ async fn test_delete_only_allowed_group(_: PgPoolOptions, options: PgConnectOpti
             "allowed_ips": "10.1.1.0/24",
             "dns": "1.1.1.1",
             "allowed_groups": ["allowed group"],
-            "mfa_enabled": false,
             "keepalive_interval": 25,
             "peer_disconnect_threshold": 180,
             "acl_enabled": false,
-            "acl_default_allow": false
+            "acl_default_allow": false,
+            "location_mfa_mode": "disabled"
         }))
         .send()
         .await;
diff --git a/crates/defguard_core/tests/integration/wireguard_network_devices.rs b/crates/defguard_core/tests/integration/wireguard_network_devices.rs
index b46c71ea5d..7fcd849585 100644
--- a/crates/defguard_core/tests/integration/wireguard_network_devices.rs
+++ b/crates/defguard_core/tests/integration/wireguard_network_devices.rs
@@ -22,11 +22,11 @@ fn make_network() -> Value {
         "allowed_ips": "10.1.1.0/24",
         "dns": "1.1.1.1",
         "allowed_groups": [],
-        "mfa_enabled": false,
         "keepalive_interval": 25,
         "peer_disconnect_threshold": 180,
         "acl_enabled": false,
-        "acl_default_allow": false
+        "acl_default_allow": false,
+        "location_mfa_mode": "disabled"
     })
 }
 
@@ -39,11 +39,11 @@ fn make_second_network() -> Value {
         "allowed_ips": "10.6.1.0/24",
         "dns": "1.1.1.1",
         "allowed_groups": [],
-        "mfa_enabled": false,
         "keepalive_interval": 25,
         "peer_disconnect_threshold": 180,
         "acl_enabled": false,
-        "acl_default_allow": false
+        "acl_default_allow": false,
+        "location_mfa_mode": "disabled"
     })
 }
 
diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts
index ddd2ffd72d..0a5c734f35 100644
--- a/web/src/i18n/en/index.ts
+++ b/web/src/i18n/en/index.ts
@@ -2019,9 +2019,6 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
           label: 'Allowed groups',
           placeholder: 'All groups',
         },
-        mfa_enabled: {
-          label: 'Require MFA for this Location',
-        },
         keepalive_interval: {
           label: 'Keepalive interval [seconds]',
         },
diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts
index a95816a235..4a6ff495b8 100644
--- a/web/src/i18n/i18n-types.ts
+++ b/web/src/i18n/i18n-types.ts
@@ -4846,12 +4846,6 @@ type RootTranslation = {
 					 */
 					placeholder: string
 				}
-				mfa_enabled: {
-					/**
-					 * R​e​q​u​i​r​e​ ​M​F​A​ ​f​o​r​ ​t​h​i​s​ ​L​o​c​a​t​i​o​n
-					 */
-					label: string
-				}
 				keepalive_interval: {
 					/**
 					 * K​e​e​p​a​l​i​v​e​ ​i​n​t​e​r​v​a​l​ ​[​s​e​c​o​n​d​s​]
@@ -11403,12 +11397,6 @@ export type TranslationFunctions = {
 					 */
 					placeholder: () => LocalizedString
 				}
-				mfa_enabled: {
-					/**
-					 * Require MFA for this Location
-					 */
-					label: () => LocalizedString
-				}
 				keepalive_interval: {
 					/**
 					 * Keepalive interval [seconds]
diff --git a/web/src/i18n/ko/index.ts b/web/src/i18n/ko/index.ts
index d1017df4f9..4dbb5bcf7f 100644
--- a/web/src/i18n/ko/index.ts
+++ b/web/src/i18n/ko/index.ts
@@ -1450,9 +1450,6 @@ const translation: PartialDeep<Translation> = {
           label: '허용된 그룹',
           placeholder: '모든 그룹',
         },
-        mfa_enabled: {
-          label: '이 위치에 MFA 필요',
-        },
         keepalive_interval: {
           label: 'Keepalive 간격 [초]',
         },
diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts
index 8d69148ad7..5035b7823f 100644
--- a/web/src/shared/types.ts
+++ b/web/src/shared/types.ts
@@ -1380,10 +1380,10 @@ export type DeviceConfigurationResponse = {
   config: string;
   endpoint: string;
   keepalive_interval: number;
-  mfa_enabled: boolean;
   network_id: number;
   network_name: string;
   pubkey: string;
+  location_mfa_mode: LocationMfaMode;
 };
 
 export type CreateStandaloneDeviceResponse = {

From 285ed43566d568af39aae359cb9fbb96d52a80b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Fri, 18 Jul 2025 13:25:22 +0200
Subject: [PATCH 21/26] skip e2e tests until final UI is implemented

---
 e2e/tests/externalopenid.spec.ts                       | 3 +--
 e2e/tests/externalopenidmfa.spec.ts                    | 4 ++--
 e2e/types.ts                                           | 2 +-
 e2e/utils/controllers/openid/createExternalProvider.ts | 7 -------
 4 files changed, 4 insertions(+), 12 deletions(-)

diff --git a/e2e/tests/externalopenid.spec.ts b/e2e/tests/externalopenid.spec.ts
index 32d4dd8684..9e7ba3bc5f 100644
--- a/e2e/tests/externalopenid.spec.ts
+++ b/e2e/tests/externalopenid.spec.ts
@@ -24,7 +24,6 @@ test.describe('External OIDC.', () => {
       'http://localhost:8080/openid/callback',
     ],
     scopes: ['openid', 'profile', 'email'],
-    use_external_openid_mfa: false,
   };
 
   const testNetwork: NetworkForm = {
@@ -55,7 +54,7 @@ test.describe('External OIDC.', () => {
     dockerDown();
   });
 
-  test('Login through external oidc.', async ({ page }) => {
+  test.fixme('Login through external oidc.', async ({ page }) => {
     expect(client.clientID).toBeDefined();
     expect(client.clientSecret).toBeDefined();
     await waitForBase(page);
diff --git a/e2e/tests/externalopenidmfa.spec.ts b/e2e/tests/externalopenidmfa.spec.ts
index 687ff2c0b0..b83b9d3b59 100644
--- a/e2e/tests/externalopenidmfa.spec.ts
+++ b/e2e/tests/externalopenidmfa.spec.ts
@@ -21,7 +21,6 @@ test.describe('External OIDC.', () => {
     name: 'test 01',
     redirectURL: ['http://localhost:8080/openid/mfa/callback'],
     scopes: ['openid', 'profile', 'email'],
-    use_external_openid_mfa: true,
   };
 
   const testNetwork: NetworkForm = {
@@ -29,6 +28,7 @@ test.describe('External OIDC.', () => {
     address: '10.10.10.1/24',
     endpoint: '127.0.0.1',
     port: '5055',
+    location_mfa_mode: 'external',
   };
 
   test.beforeEach(async ({ browser }) => {
@@ -52,7 +52,7 @@ test.describe('External OIDC.', () => {
     dockerDown();
   });
 
-  test('Complete client MFA through external OpenID', async ({ page, browser }) => {
+  test.fixme('Complete client MFA through external OpenID', async ({ page, browser }) => {
     await waitForBase(page);
     const mfaStartUrl = `${testsConfig.ENROLLMENT_URL}/api/v1/client-mfa/start`;
     await createDevice(browser, testUser, {
diff --git a/e2e/types.ts b/e2e/types.ts
index 10acd4915c..0ba8de96ab 100644
--- a/e2e/types.ts
+++ b/e2e/types.ts
@@ -60,7 +60,6 @@ export type OpenIdClient = {
   clientSecret?: string;
   redirectURL: string[];
   scopes: OpenIdScope[];
-  use_external_openid_mfa: boolean;
 };
 
 export type NetworkForm = {
@@ -70,6 +69,7 @@ export type NetworkForm = {
   port: string;
   allowed_ips?: string;
   dns?: string;
+  location_mfa_mode?:string;
 };
 
 export type DeviceForm = {
diff --git a/e2e/utils/controllers/openid/createExternalProvider.ts b/e2e/utils/controllers/openid/createExternalProvider.ts
index 89dd0fb5fa..277c7e5e5c 100644
--- a/e2e/utils/controllers/openid/createExternalProvider.ts
+++ b/e2e/utils/controllers/openid/createExternalProvider.ts
@@ -18,13 +18,6 @@ export const createExternalProvider = async (browser: Browser, client: OpenIdCli
   await page.getByTestId('field-client_id').fill(client.clientID || '');
   await page.getByTestId('field-client_secret').fill(client.clientSecret || '');
   await page.getByTestId('field-display_name').fill(client.name);
-  if (client.use_external_openid_mfa) {
-    const checkbox = page
-      .locator('div')
-      .filter({ hasText: /^Use external OpenID for client MFA$/ })
-      .nth(1);
-    await checkbox.click();
-  }
   await page.getByRole('button', { name: 'Save changes' }).click();
   await context.close();
 };

From 6d86f150278bca34fa839dd17840e1d616cf41c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Fri, 18 Jul 2025 13:52:23 +0200
Subject: [PATCH 22/26] add styled MFA mode select

---
 .../FormLocationMfaModeSelect.tsx             | 41 ++++++++++++-----
 .../Form/FormLocationMfaModeSelect/style.scss | 45 +++++++++++++++++++
 2 files changed, 76 insertions(+), 10 deletions(-)
 create mode 100644 web/src/shared/components/Form/FormLocationMfaModeSelect/style.scss

diff --git a/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
index c0600c22c4..fe8be3d3f9 100644
--- a/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
+++ b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
@@ -1,8 +1,13 @@
+import './style.scss';
+import clsx from 'clsx';
 import { useMemo } from 'react';
-import type { FieldValues, UseControllerProps } from 'react-hook-form';
-
+import {
+  type FieldValues,
+  type UseControllerProps,
+  useController,
+} from 'react-hook-form';
 import { useI18nContext } from '../../../../i18n/i18n-react';
-import { FormSelect } from '../../../defguard-ui/components/Form/FormSelect/FormSelect';
+import { RadioButton } from '../../../defguard-ui/components/Layout/RadioButton/Radiobutton';
 import type { SelectOption } from '../../../defguard-ui/components/Layout/Select/types';
 import { LocationMfaMode } from '../../../types';
 
@@ -13,9 +18,11 @@ type Props<T extends FieldValues> = {
 
 export const FormLocationMfaModeSelect = <T extends FieldValues>({
   controller,
-  disabled = false,
 }: Props<T>) => {
   const { LL } = useI18nContext();
+  const {
+    field: { onChange, value: fieldValue },
+  } = useController(controller);
 
   const options = useMemo(
     (): SelectOption<LocationMfaMode>[] => [
@@ -37,12 +44,26 @@ export const FormLocationMfaModeSelect = <T extends FieldValues>({
     ],
     [LL.components.aclDefaultPolicySelect.options],
   );
+
   return (
-    <FormSelect
-      controller={controller}
-      options={options}
-      label={LL.components.locationMfaModeSelect.label()}
-      disabled={disabled}
-    />
+    <div className="location-mfa-mode-select">
+      {options.map(({ key, value, label }) => {
+        const active = fieldValue === value;
+        return (
+          <div
+            className={clsx('location-mfa-mode', {
+              active,
+            })}
+            key={key}
+            onClick={() => {
+              onChange(value);
+            }}
+          >
+            <p className="label">{label}</p>
+            <RadioButton active={active} />
+          </div>
+        );
+      })}
+    </div>
   );
 };
diff --git a/web/src/shared/components/Form/FormLocationMfaModeSelect/style.scss b/web/src/shared/components/Form/FormLocationMfaModeSelect/style.scss
new file mode 100644
index 0000000000..60957ca969
--- /dev/null
+++ b/web/src/shared/components/Form/FormLocationMfaModeSelect/style.scss
@@ -0,0 +1,45 @@
+.location-mfa-mode-select {
+  display: flex;
+  flex-flow: column;
+  row-gap: var(--spacing-s);
+
+  .location-mfa-mode {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    column-gap: var(--spacing-xs);
+    min-height: 30px;
+    border: 1px solid var(--border-primary);
+    padding: var(--spacing-xs) var(--spacing-s);
+    border-radius: 10px;
+    cursor: pointer;
+    user-select: none;
+    transition-property: border-color;
+
+    @include animate-standard;
+
+    &:not(.active) {
+      &:hover {
+        border-color: var(--border-separator);
+      }
+    }
+
+    &.active {
+      border-color: var(--surface-main-primary);
+    }
+
+    &.active,
+    &:hover {
+      .label {
+        color: var(--text-body-primary);
+      }
+    }
+
+    .label {
+      color: var(--text-body-secondary);
+      transition-property: color;
+      @include typography(app-modal-1);
+      @include animate-standard;
+    }
+  }
+}
\ No newline at end of file

From 734711d21099024a2b60fb04887f53e7291bbf95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Mon, 21 Jul 2025 10:57:18 +0200
Subject: [PATCH 23/26] add message box and section header

---
 web/src/i18n/en/index.ts                      |  8 ++++
 web/src/i18n/i18n-types.ts                    | 40 +++++++++++++++++++
 .../NetworkEditForm/NetworkEditForm.tsx       | 18 +++++++++
 .../components/DividerHeader.tsx              | 16 ++++++++
 .../pages/network/NetworkEditForm/style.scss  | 29 ++++++++++++++
 .../FormLocationMfaModeSelect.tsx             |  1 -
 .../Form/FormLocationMfaModeSelect/style.scss |  2 +-
 7 files changed, 112 insertions(+), 2 deletions(-)
 create mode 100644 web/src/pages/network/NetworkEditForm/components/DividerHeader.tsx

diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts
index 0a5c734f35..dc0f928f06 100644
--- a/web/src/i18n/en/index.ts
+++ b/web/src/i18n/en/index.ts
@@ -1991,6 +1991,11 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
           'By default, all users will be allowed to connect to this location. If you want to restrict access to this location to a specific group, please select it below.',
         aclFeatureDisabled:
           "ACL functionality is an enterprise feature and you've exceeded the user, device or network limits to use it. In order to use this feature, purchase an enterprise license or upgrade your existing one.",
+        locationMfaMode: {
+          description: 'Choose how MFA is enforced when connecting to this location:', 
+          internal: "Internal MFA - MFA is enforced using Defguard's built-in MFA (e.g. TOTP, WebAuthn) with internal identity",
+          external: 'External MFA - If configured (see [OpenID settings](settings)) this option uses external identity provider for MFA',
+        },
       },
       messages: {
         networkModified: 'Location modified.',
@@ -2031,6 +2036,9 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
         acl_default_allow: {
           label: 'Default ACL policy',
         },
+        location_mfa_mode: {
+          label: 'MFA requirement',
+        }
       },
       controls: {
         submit: 'Save changes',
diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts
index 4a6ff495b8..ce89aa98e7 100644
--- a/web/src/i18n/i18n-types.ts
+++ b/web/src/i18n/i18n-types.ts
@@ -4788,6 +4788,20 @@ type RootTranslation = {
 				 * A​C​L​ ​f​u​n​c​t​i​o​n​a​l​i​t​y​ ​i​s​ ​a​n​ ​e​n​t​e​r​p​r​i​s​e​ ​f​e​a​t​u​r​e​ ​a​n​d​ ​y​o​u​'​v​e​ ​e​x​c​e​e​d​e​d​ ​t​h​e​ ​u​s​e​r​,​ ​d​e​v​i​c​e​ ​o​r​ ​n​e​t​w​o​r​k​ ​l​i​m​i​t​s​ ​t​o​ ​u​s​e​ ​i​t​.​ ​I​n​ ​o​r​d​e​r​ ​t​o​ ​u​s​e​ ​t​h​i​s​ ​f​e​a​t​u​r​e​,​ ​p​u​r​c​h​a​s​e​ ​a​n​ ​e​n​t​e​r​p​r​i​s​e​ ​l​i​c​e​n​s​e​ ​o​r​ ​u​p​g​r​a​d​e​ ​y​o​u​r​ ​e​x​i​s​t​i​n​g​ ​o​n​e​.
 				 */
 				aclFeatureDisabled: string
+				locationMfaMode: {
+					/**
+					 * C​h​o​o​s​e​ ​h​o​w​ ​M​F​A​ ​i​s​ ​e​n​f​o​r​c​e​d​ ​w​h​e​n​ ​c​o​n​n​e​c​t​i​n​g​ ​t​o​ ​t​h​i​s​ ​l​o​c​a​t​i​o​n​:
+					 */
+					description: string
+					/**
+					 * I​n​t​e​r​n​a​l​ ​M​F​A​ ​-​ ​M​F​A​ ​i​s​ ​e​n​f​o​r​c​e​d​ ​u​s​i​n​g​ ​D​e​f​g​u​a​r​d​'​s​ ​b​u​i​l​t​-​i​n​ ​M​F​A​ ​(​e​.​g​.​ ​T​O​T​P​,​ ​W​e​b​A​u​t​h​n​)​ ​w​i​t​h​ ​i​n​t​e​r​n​a​l​ ​i​d​e​n​t​i​t​y
+					 */
+					internal: string
+					/**
+					 * E​x​t​e​r​n​a​l​ ​M​F​A​ ​-​ ​I​f​ ​c​o​n​f​i​g​u​r​e​d​ ​(​s​e​e​ ​[​O​p​e​n​I​D​ ​s​e​t​t​i​n​g​s​]​(​s​e​t​t​i​n​g​s​)​)​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​u​s​e​s​ ​e​x​t​e​r​n​a​l​ ​i​d​e​n​t​i​t​y​ ​p​r​o​v​i​d​e​r​ ​f​o​r​ ​M​F​A
+					 */
+					external: string
+				}
 			}
 			messages: {
 				/**
@@ -4870,6 +4884,12 @@ type RootTranslation = {
 					 */
 					label: string
 				}
+				location_mfa_mode: {
+					/**
+					 * M​F​A​ ​r​e​q​u​i​r​e​m​e​n​t
+					 */
+					label: string
+				}
 			}
 			controls: {
 				/**
@@ -11339,6 +11359,20 @@ export type TranslationFunctions = {
 				 * ACL functionality is an enterprise feature and you've exceeded the user, device or network limits to use it. In order to use this feature, purchase an enterprise license or upgrade your existing one.
 				 */
 				aclFeatureDisabled: () => LocalizedString
+				locationMfaMode: {
+					/**
+					 * Choose how MFA is enforced when connecting to this location:
+					 */
+					description: () => LocalizedString
+					/**
+					 * Internal MFA - MFA is enforced using Defguard's built-in MFA (e.g. TOTP, WebAuthn) with internal identity
+					 */
+					internal: () => LocalizedString
+					/**
+					 * External MFA - If configured (see [OpenID settings](settings)) this option uses external identity provider for MFA
+					 */
+					external: () => LocalizedString
+				}
 			}
 			messages: {
 				/**
@@ -11421,6 +11455,12 @@ export type TranslationFunctions = {
 					 */
 					label: () => LocalizedString
 				}
+				location_mfa_mode: {
+					/**
+					 * MFA requirement
+					 */
+					label: () => LocalizedString
+				}
 			}
 			controls: {
 				/**
diff --git a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
index 3e8a2994e4..69fad36c22 100644
--- a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
+++ b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
@@ -12,6 +12,7 @@ import { shallow } from 'zustand/shallow';
 import { useI18nContext } from '../../../i18n/i18n-react';
 import { FormAclDefaultPolicy } from '../../../shared/components/Form/FormAclDefaultPolicySelect/FormAclDefaultPolicy.tsx';
 import { FormLocationMfaModeSelect } from '../../../shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx';
+import { RenderMarkdown } from '../../../shared/components/Layout/RenderMarkdown/RenderMarkdown.tsx';
 import { FormCheckBox } from '../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx';
 import { FormInput } from '../../../shared/defguard-ui/components/Form/FormInput/FormInput';
 import { FormSelect } from '../../../shared/defguard-ui/components/Form/FormSelect/FormSelect';
@@ -31,6 +32,7 @@ import {
   validateIpOrDomainList,
 } from '../../../shared/validators';
 import { useNetworkPageStore } from '../hooks/useNetworkPageStore';
+import { DividerHeader } from './components/DividerHeader.tsx';
 
 export const NetworkEditForm = () => {
   const toaster = useToaster();
@@ -341,6 +343,22 @@ export const NetworkEditForm = () => {
           label={LL.networkConfiguration.form.fields.peer_disconnect_threshold.label()}
           type="number"
         />
+        <DividerHeader
+          text={LL.networkConfiguration.form.fields.location_mfa_mode.label()}
+        />
+        <MessageBox id="location-mfa-mode-explain-message-box">
+          <p>{LL.networkConfiguration.form.helpers.locationMfaMode.description()}</p>
+          <ul>
+            <li>
+              <p>{LL.networkConfiguration.form.helpers.locationMfaMode.internal()}</p>
+            </li>
+            <li>
+              <RenderMarkdown
+                content={LL.networkConfiguration.form.helpers.locationMfaMode.external()}
+              />
+            </li>
+          </ul>
+        </MessageBox>
         <FormLocationMfaModeSelect controller={{ control, name: 'location_mfa_mode' }} />
         <button type="submit" className="hidden" ref={submitRef}></button>
       </form>
diff --git a/web/src/pages/network/NetworkEditForm/components/DividerHeader.tsx b/web/src/pages/network/NetworkEditForm/components/DividerHeader.tsx
new file mode 100644
index 0000000000..86243ea08b
--- /dev/null
+++ b/web/src/pages/network/NetworkEditForm/components/DividerHeader.tsx
@@ -0,0 +1,16 @@
+import type { PropsWithChildren } from 'react';
+
+type DividerHeaderProps = {
+  text: string;
+} & PropsWithChildren;
+
+export const DividerHeader = ({ text, children }: DividerHeaderProps) => {
+  return (
+    <div className="divider-header spacer">
+      <div className="inner">
+        <p className="header">{text}</p>
+        {children}
+      </div>
+    </div>
+  );
+};
diff --git a/web/src/pages/network/NetworkEditForm/style.scss b/web/src/pages/network/NetworkEditForm/style.scss
index deaf9ead94..c8840e9d88 100644
--- a/web/src/pages/network/NetworkEditForm/style.scss
+++ b/web/src/pages/network/NetworkEditForm/style.scss
@@ -29,4 +29,33 @@
       }
     }
   }
+
+  #location-mfa-mode-explain-message-box {
+    ul {
+      list-style-position: inside;
+      margin-top: 8px;
+
+      li {
+        p {
+          display: inline;
+        }
+      }
+    }
+  }
+
+  .divider-header {
+    padding-bottom: var(--spacing-s);
+
+    .inner {
+      display: flex;
+      flex-flow: row;
+      align-items: center;
+      justify-content: flex-start;
+      border-bottom: 1px solid var(--border-primary);
+    }
+
+    .header {
+      @include typography(app-side-bar);
+    }
+  }
 }
diff --git a/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
index fe8be3d3f9..65640ed116 100644
--- a/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
+++ b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
@@ -13,7 +13,6 @@ import { LocationMfaMode } from '../../../types';
 
 type Props<T extends FieldValues> = {
   controller: UseControllerProps<T>;
-  disabled?: boolean;
 };
 
 export const FormLocationMfaModeSelect = <T extends FieldValues>({
diff --git a/web/src/shared/components/Form/FormLocationMfaModeSelect/style.scss b/web/src/shared/components/Form/FormLocationMfaModeSelect/style.scss
index 60957ca969..62799c65d1 100644
--- a/web/src/shared/components/Form/FormLocationMfaModeSelect/style.scss
+++ b/web/src/shared/components/Form/FormLocationMfaModeSelect/style.scss
@@ -42,4 +42,4 @@
       @include animate-standard;
     }
   }
-}
\ No newline at end of file
+}

From a224bfa69b59ab59ce2c6f97717b0e2e2969e33b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Mon, 21 Jul 2025 11:01:34 +0200
Subject: [PATCH 24/26] reenable e2e openid tests

---
 e2e/tests/externalopenid.spec.ts    | 2 +-
 e2e/tests/externalopenidmfa.spec.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/e2e/tests/externalopenid.spec.ts b/e2e/tests/externalopenid.spec.ts
index 9e7ba3bc5f..29cbf60aac 100644
--- a/e2e/tests/externalopenid.spec.ts
+++ b/e2e/tests/externalopenid.spec.ts
@@ -54,7 +54,7 @@ test.describe('External OIDC.', () => {
     dockerDown();
   });
 
-  test.fixme('Login through external oidc.', async ({ page }) => {
+  test('Login through external oidc.', async ({ page }) => {
     expect(client.clientID).toBeDefined();
     expect(client.clientSecret).toBeDefined();
     await waitForBase(page);
diff --git a/e2e/tests/externalopenidmfa.spec.ts b/e2e/tests/externalopenidmfa.spec.ts
index b83b9d3b59..785e7cc1a1 100644
--- a/e2e/tests/externalopenidmfa.spec.ts
+++ b/e2e/tests/externalopenidmfa.spec.ts
@@ -52,7 +52,7 @@ test.describe('External OIDC.', () => {
     dockerDown();
   });
 
-  test.fixme('Complete client MFA through external OpenID', async ({ page, browser }) => {
+  test('Complete client MFA through external OpenID', async ({ page, browser }) => {
     await waitForBase(page);
     const mfaStartUrl = `${testsConfig.ENROLLMENT_URL}/api/v1/client-mfa/start`;
     await createDevice(browser, testUser, {

From 53ac122864209b4a9376e4843a288dee9f05181a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Mon, 21 Jul 2025 12:28:09 +0200
Subject: [PATCH 25/26] fix mfa e2e test

---
 e2e/types.ts                                          |  2 +-
 e2e/utils/controllers/vpn/createNetwork.ts            | 11 ++++++++++-
 .../FormLocationMfaModeSelect.tsx                     |  2 +-
 3 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/e2e/types.ts b/e2e/types.ts
index 0ba8de96ab..966f45e8b4 100644
--- a/e2e/types.ts
+++ b/e2e/types.ts
@@ -69,7 +69,7 @@ export type NetworkForm = {
   port: string;
   allowed_ips?: string;
   dns?: string;
-  location_mfa_mode?:string;
+  location_mfa_mode?: string;
 };
 
 export type DeviceForm = {
diff --git a/e2e/utils/controllers/vpn/createNetwork.ts b/e2e/utils/controllers/vpn/createNetwork.ts
index 7351a4a4e5..9fed2c3c7e 100644
--- a/e2e/utils/controllers/vpn/createNetwork.ts
+++ b/e2e/utils/controllers/vpn/createNetwork.ts
@@ -15,11 +15,20 @@ export const createNetwork = async (browser: Browser, network: NetworkForm) => {
   const navNext = page.getByTestId('wizard-next');
   await page.getByTestId('setup-option-manual').click();
   await navNext.click();
-  for (const key of Object.keys(network)) {
+
+  // fill form
+  for (const key of Object.keys(network).filter(key => key !== 'location_mfa_mode')) {
     const field = page.getByTestId(`field-${key}`);
     await field.clear();
     await field.type(network[key]);
   }
+  // select location MFA mode
+  if (network.location_mfa_mode) {
+    const mfaModeSelect = page.locator("div.location-mfa-mode-select");
+    const mfaMode = mfaModeSelect.locator(`div.${network.location_mfa_mode}`);
+    await mfaMode.click();
+  }
+  
   const responseCreateNetworkPromise = page.waitForResponse('**/network');
   await navNext.click();
   const response = await responseCreateNetworkPromise;
diff --git a/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
index 65640ed116..ff11d7f350 100644
--- a/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
+++ b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx
@@ -50,7 +50,7 @@ export const FormLocationMfaModeSelect = <T extends FieldValues>({
         const active = fieldValue === value;
         return (
           <div
-            className={clsx('location-mfa-mode', {
+            className={clsx(`location-mfa-mode ${value}`, {
               active,
             })}
             key={key}

From bda770811f84871a8f116d1f2373a1d690ecab18 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= <maciek@wjck.pl>
Date: Mon, 21 Jul 2025 12:57:24 +0200
Subject: [PATCH 26/26] formatting

---
 e2e/package.json                           | 4 ++--
 e2e/utils/controllers/vpn/createNetwork.ts | 6 +++---
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/e2e/package.json b/e2e/package.json
index 209237b493..9d01651436 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -5,7 +5,7 @@
   "type": "commonjs",
   "scripts": {
     "lint": "pnpm prettier --check './tests/**.ts' './utils/**/*.ts' && pnpm eslint './tests/**.ts' './utils/**/*.ts'",
-    "fix": "pnpm prettier -w ./tests/**/*.ts ./utils/**/*.ts && pnpm eslint --fix ./tests/**/*.ts ./utils/**/*.ts",
+    "fix": "pnpm prettier -w './tests/**.ts' './utils/**/*.ts' && pnpm eslint --fix ./tests/**/*.ts ./utils/**/*.ts",
     "test": "pnpm playwright test"
   },
   "keywords": [],
@@ -42,4 +42,4 @@
   "volta": {
     "node": "19.9.0"
   }
-}
+}
\ No newline at end of file
diff --git a/e2e/utils/controllers/vpn/createNetwork.ts b/e2e/utils/controllers/vpn/createNetwork.ts
index 9fed2c3c7e..b17a059872 100644
--- a/e2e/utils/controllers/vpn/createNetwork.ts
+++ b/e2e/utils/controllers/vpn/createNetwork.ts
@@ -17,18 +17,18 @@ export const createNetwork = async (browser: Browser, network: NetworkForm) => {
   await navNext.click();
 
   // fill form
-  for (const key of Object.keys(network).filter(key => key !== 'location_mfa_mode')) {
+  for (const key of Object.keys(network).filter((key) => key !== 'location_mfa_mode')) {
     const field = page.getByTestId(`field-${key}`);
     await field.clear();
     await field.type(network[key]);
   }
   // select location MFA mode
   if (network.location_mfa_mode) {
-    const mfaModeSelect = page.locator("div.location-mfa-mode-select");
+    const mfaModeSelect = page.locator('div.location-mfa-mode-select');
     const mfaMode = mfaModeSelect.locator(`div.${network.location_mfa_mode}`);
     await mfaMode.click();
   }
-  
+
   const responseCreateNetworkPromise = page.waitForResponse('**/network');
   await navNext.click();
   const response = await responseCreateNetworkPromise;
