From 71fa5236943293c458ec6d94751fd0f52acc91bb Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 22 Mar 2026 00:01:29 +0100 Subject: [PATCH 1/6] enhance settings page with force option --- two-factor.php | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/two-factor.php b/two-factor.php index 6be20e19..47d39386 100644 --- a/two-factor.php +++ b/two-factor.php @@ -181,3 +181,101 @@ function two_factor_filter_enabled_providers_for_user( $enabled, $user_id ) { return array_values( array_intersect( (array) $enabled, $site_enabled ) ); } + +/** + * Enforce Two-Factor for users in roles that require it. + * + * Runs after the site-enabled filter (priority 20). If a user belongs to an + * enforced role but has no providers configured, email verification is + * injected so they are prompted to authenticate on their next login. + * + * @since 0.16 + * + * @param array $enabled Currently enabled provider classnames for the user. + * @param int $user_id The user ID. + * @return array + */ +function two_factor_enforce_for_user( $enabled, $user_id ) { + // User already has at least one provider — nothing to enforce. + if ( ! empty( $enabled ) ) { + return $enabled; + } + + $enforced_roles = (array) get_option( 'two_factor_enforced_roles', array() ); + if ( empty( $enforced_roles ) ) { + return $enabled; + } + + $user = get_userdata( $user_id ); + if ( ! $user ) { + return $enabled; + } + + // Check whether the user has at least one enforced role. + if ( empty( array_intersect( (array) $user->roles, $enforced_roles ) ) ) { + return $enabled; + } + + // Determine which provider to inject. Prefer Email; fall back to the + // first site-enabled provider if Email has been disabled site-wide. + $site_enabled = two_factor_get_enabled_providers_option(); + + if ( null === $site_enabled || in_array( 'Two_Factor_Email', $site_enabled, true ) ) { + return array( 'Two_Factor_Email' ); + } + + // Email is not site-enabled; use the first available site-enabled provider. + if ( ! empty( $site_enabled ) ) { + return array( $site_enabled[0] ); + } + + return $enabled; +} +add_filter( 'two_factor_enabled_providers_for_user', 'two_factor_enforce_for_user', 20, 2 ); + +/** + * Auto-configure email verification for new users in enforced roles. + * + * Fires immediately after a new user account is created so that enforced + * users are required to verify via email on their very first login. + * + * @since 0.16 + * + * @param int $user_id The newly registered user ID. + * @return void + */ +function two_factor_force_on_user_register( $user_id ) { + $enforced_roles = (array) get_option( 'two_factor_enforced_roles', array() ); + if ( empty( $enforced_roles ) ) { + return; + } + + $user = get_userdata( $user_id ); + if ( ! $user ) { + return; + } + + if ( empty( array_intersect( (array) $user->roles, $enforced_roles ) ) ) { + return; + } + + // Do not overwrite a configuration that already exists. + $existing = get_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, true ); + if ( ! empty( $existing ) ) { + return; + } + + // Determine which provider to assign (same logic as the runtime filter). + $site_enabled = two_factor_get_enabled_providers_option(); + $provider = 'Two_Factor_Email'; + + if ( null !== $site_enabled && ! in_array( 'Two_Factor_Email', $site_enabled, true ) ) { + $provider = ! empty( $site_enabled ) ? $site_enabled[0] : null; + } + + if ( $provider ) { + update_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( $provider ) ); + update_user_meta( $user_id, Two_Factor_Core::PROVIDER_USER_META_KEY, $provider ); + } +} +add_action( 'user_register', 'two_factor_force_on_user_register', 10, 1 ); From d742d16b2041f6606092b5edbef394eefcfc604d Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 22 Mar 2026 00:01:32 +0100 Subject: [PATCH 2/6] enhance settings page with force option --- settings/class-two-factor-settings.php | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/settings/class-two-factor-settings.php b/settings/class-two-factor-settings.php index cb7b7132..26702e17 100644 --- a/settings/class-two-factor-settings.php +++ b/settings/class-two-factor-settings.php @@ -41,6 +41,11 @@ public static function render_settings_page() { update_option( 'two_factor_enabled_providers', array_values( array_unique( $enabled ) ) ); + $enforced_roles_posted = isset( $_POST['two_factor_enforced_roles'] ) && is_array( $_POST['two_factor_enforced_roles'] ) + ? array_map( 'sanitize_key', wp_unslash( $_POST['two_factor_enforced_roles'] ) ) + : array(); + update_option( 'two_factor_enforced_roles', array_values( array_unique( $enforced_roles_posted ) ) ); + echo '

' . esc_html__( 'Settings saved.', 'two-factor' ) . '

'; } @@ -86,6 +91,34 @@ public static function render_settings_page() { echo ''; } + echo ''; + + echo ''; + + // --- Enforcement section --- + $saved_enforced_roles = (array) get_option( 'two_factor_enforced_roles', array() ); + $all_roles = wp_roles()->get_names(); + + echo '

' . esc_html__( 'Two-Factor Enforcement', 'two-factor' ) . '

'; + echo '

' . esc_html__( 'Require Two-Factor authentication for specific user roles. Users in enforced roles who have not yet configured 2FA will automatically be challenged via email verification on login. New users in enforced roles will also have email verification enabled on registration.', 'two-factor' ) . '

'; + + echo '
' . esc_html__( 'Enforced Roles', 'two-factor' ) . ''; + echo ''; + + if ( empty( $all_roles ) ) { + echo ''; + } else { + echo ''; + } + echo '
' . esc_html__( 'No roles found.', 'two-factor' ) . '
'; + foreach ( $all_roles as $role_slug => $role_name ) { + $role_slug = sanitize_key( $role_slug ); + echo '

'; + } + echo '
'; echo '
'; From 55b5d1e13580a8e50aec1332333b21ab16981104 Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 22 Mar 2026 00:24:58 +0100 Subject: [PATCH 3/6] add uninstall enhancements --- class-two-factor-core.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 7b57b868..0a24610e 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -227,6 +227,10 @@ public static function uninstall() { } } + // Add site-level options owned by the settings screen. + $option_keys[] = 'two_factor_enabled_providers'; + $option_keys[] = 'two_factor_enforced_roles'; + // Delete options first since that is faster. if ( ! empty( $option_keys ) ) { foreach ( $option_keys as $option_key ) { From 600ad521215b3a9b9c587b72c8c9fe4eb8765bd7 Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 22 Mar 2026 00:25:33 +0100 Subject: [PATCH 4/6] update copilot feedback --- two-factor.php | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/two-factor.php b/two-factor.php index 47d39386..c1cd8ff2 100644 --- a/two-factor.php +++ b/two-factor.php @@ -186,8 +186,12 @@ function two_factor_filter_enabled_providers_for_user( $enabled, $user_id ) { * Enforce Two-Factor for users in roles that require it. * * Runs after the site-enabled filter (priority 20). If a user belongs to an - * enforced role but has no providers configured, email verification is - * injected so they are prompted to authenticate on their next login. + * enforced role but has no providers configured, the Email provider is injected + * so they are prompted to authenticate on their next login. + * + * Enforcement only activates when the Email provider is site-enabled. Other + * providers (e.g. TOTP) require per-user setup before they can authenticate, + * so injecting them without prior configuration would silently fail open. * * @since 0.16 * @@ -216,20 +220,16 @@ function two_factor_enforce_for_user( $enabled, $user_id ) { return $enabled; } - // Determine which provider to inject. Prefer Email; fall back to the - // first site-enabled provider if Email has been disabled site-wide. + // Only enforce via Email. Other providers require prior user setup and + // cannot be injected without risking a silent fail-open. $site_enabled = two_factor_get_enabled_providers_option(); - - if ( null === $site_enabled || in_array( 'Two_Factor_Email', $site_enabled, true ) ) { - return array( 'Two_Factor_Email' ); - } - - // Email is not site-enabled; use the first available site-enabled provider. - if ( ! empty( $site_enabled ) ) { - return array( $site_enabled[0] ); + if ( null !== $site_enabled && ! in_array( 'Two_Factor_Email', $site_enabled, true ) ) { + // Email is disabled site-wide — cannot enforce safely. The settings + // page will display a warning to the administrator. + return $enabled; } - return $enabled; + return array( 'Two_Factor_Email' ); } add_filter( 'two_factor_enabled_providers_for_user', 'two_factor_enforce_for_user', 20, 2 ); @@ -265,17 +265,14 @@ function two_factor_force_on_user_register( $user_id ) { return; } - // Determine which provider to assign (same logic as the runtime filter). + // Only enrol via Email — same constraint as the runtime enforcement filter. $site_enabled = two_factor_get_enabled_providers_option(); - $provider = 'Two_Factor_Email'; - if ( null !== $site_enabled && ! in_array( 'Two_Factor_Email', $site_enabled, true ) ) { - $provider = ! empty( $site_enabled ) ? $site_enabled[0] : null; + // Email is disabled site-wide; skip auto-enrolment. + return; } - if ( $provider ) { - update_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( $provider ) ); - update_user_meta( $user_id, Two_Factor_Core::PROVIDER_USER_META_KEY, $provider ); - } + update_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Email' ) ); + update_user_meta( $user_id, Two_Factor_Core::PROVIDER_USER_META_KEY, 'Two_Factor_Email' ); } add_action( 'user_register', 'two_factor_force_on_user_register', 10, 1 ); From 14d87749c97c8b02e22c595d6ab523364495fce8 Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 22 Mar 2026 00:26:29 +0100 Subject: [PATCH 5/6] show warning, concrete message --- settings/class-two-factor-settings.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/settings/class-two-factor-settings.php b/settings/class-two-factor-settings.php index 26702e17..3317a3f3 100644 --- a/settings/class-two-factor-settings.php +++ b/settings/class-two-factor-settings.php @@ -49,6 +49,18 @@ public static function render_settings_page() { echo '

' . esc_html__( 'Settings saved.', 'two-factor' ) . '

'; } + // Show a warning when enforcement is active but the Email provider is disabled, + // because enforcement relies on Email being available for users not yet enrolled. + $_enforced = (array) get_option( 'two_factor_enforced_roles', array() ); + if ( ! empty( $_enforced ) ) { + $_site_enabled = function_exists( 'two_factor_get_enabled_providers_option' ) + ? two_factor_get_enabled_providers_option() + : null; + if ( null !== $_site_enabled && ! in_array( 'Two_Factor_Email', $_site_enabled, true ) ) { + echo '

' . esc_html__( 'Two-Factor enforcement is active, but the Email provider is disabled. Users in enforced roles who have not yet set up 2FA will not be challenged on login. Enable the Email provider to ensure enforcement works.', 'two-factor' ) . '

'; + } + } + // Build provider list for display using public core API. $provider_instances = array(); if ( class_exists( 'Two_Factor_Core' ) && method_exists( 'Two_Factor_Core', 'get_providers' ) ) { @@ -100,7 +112,7 @@ public static function render_settings_page() { $all_roles = wp_roles()->get_names(); echo '

' . esc_html__( 'Two-Factor Enforcement', 'two-factor' ) . '

'; - echo '

' . esc_html__( 'Require Two-Factor authentication for specific user roles. Users in enforced roles who have not yet configured 2FA will automatically be challenged via email verification on login. New users in enforced roles will also have email verification enabled on registration.', 'two-factor' ) . '

'; + echo '

' . esc_html__( 'Require Two-Factor authentication for specific user roles. Users in enforced roles who have not yet set up 2FA will be challenged via the Email provider on login. This requires the Email provider to be enabled above. New users in enforced roles will also have the Email provider enabled on registration.', 'two-factor' ) . '

'; echo '
' . esc_html__( 'Enforced Roles', 'two-factor' ) . ''; echo ''; From 60a948e13d4ddcd1788102ece32759fe5013f8bf Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 22 Mar 2026 00:34:33 +0100 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- settings/class-two-factor-settings.php | 8 ++++---- two-factor.php | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/settings/class-two-factor-settings.php b/settings/class-two-factor-settings.php index 3317a3f3..7b546af0 100644 --- a/settings/class-two-factor-settings.php +++ b/settings/class-two-factor-settings.php @@ -51,12 +51,12 @@ public static function render_settings_page() { // Show a warning when enforcement is active but the Email provider is disabled, // because enforcement relies on Email being available for users not yet enrolled. - $_enforced = (array) get_option( 'two_factor_enforced_roles', array() ); - if ( ! empty( $_enforced ) ) { - $_site_enabled = function_exists( 'two_factor_get_enabled_providers_option' ) + $enforced_roles = (array) get_option( 'two_factor_enforced_roles', array() ); + if ( ! empty( $enforced_roles ) ) { + $site_enabled = function_exists( 'two_factor_get_enabled_providers_option' ) ? two_factor_get_enabled_providers_option() : null; - if ( null !== $_site_enabled && ! in_array( 'Two_Factor_Email', $_site_enabled, true ) ) { + if ( null !== $site_enabled && ! in_array( 'Two_Factor_Email', $site_enabled, true ) ) { echo '

' . esc_html__( 'Two-Factor enforcement is active, but the Email provider is disabled. Users in enforced roles who have not yet set up 2FA will not be challenged on login. Enable the Email provider to ensure enforcement works.', 'two-factor' ) . '

'; } } diff --git a/two-factor.php b/two-factor.php index c1cd8ff2..b535875b 100644 --- a/two-factor.php +++ b/two-factor.php @@ -272,6 +272,12 @@ function two_factor_force_on_user_register( $user_id ) { return; } + // Ensure the Email provider is actually registered/supported for this user. + $supported_providers = Two_Factor_Core::get_supported_providers_for_user( $user ); + if ( empty( $supported_providers ) || ! isset( $supported_providers['Two_Factor_Email'] ) ) { + // Email provider is not supported for this user; skip auto-enrolment. + return; + } update_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Email' ) ); update_user_meta( $user_id, Two_Factor_Core::PROVIDER_USER_META_KEY, 'Two_Factor_Email' ); }