diff --git a/class-two-factor-core.php b/class-two-factor-core.php index d98cbfe6..2517ab60 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -225,6 +225,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 ) { diff --git a/settings/class-two-factor-settings.php b/settings/class-two-factor-settings.php index 48018835..dedd7987 100644 --- a/settings/class-two-factor-settings.php +++ b/settings/class-two-factor-settings.php @@ -41,9 +41,26 @@ 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' ) . '

'; } + // 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_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 ) ) { + 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' ) ) { @@ -86,6 +103,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 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 ''; + + 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 '
'; diff --git a/two-factor.php b/two-factor.php index b1e4eca4..7f12976f 100644 --- a/two-factor.php +++ b/two-factor.php @@ -188,3 +188,104 @@ 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, 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 + * + * @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; + } + + // 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 ) ) { + // Email is disabled site-wide — cannot enforce safely. The settings + // page will display a warning to the administrator. + return $enabled; + } + + return array( 'Two_Factor_Email' ); +} +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; + } + + // Only enrol via Email — same constraint as the runtime enforcement filter. + $site_enabled = two_factor_get_enabled_providers_option(); + if ( null !== $site_enabled && ! in_array( 'Two_Factor_Email', $site_enabled, true ) ) { + // Email is disabled site-wide; skip auto-enrolment. + 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' ); +} +add_action( 'user_register', 'two_factor_force_on_user_register', 10, 1 );