Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand Down
45 changes: 45 additions & 0 deletions settings/class-two-factor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ) );

Comment thread
masteradhoc marked this conversation as resolved.
echo '<div class="updated"><p>' . esc_html__( 'Settings saved.', 'two-factor' ) . '</p></div>';
}

// 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 ) ) {
Comment thread
masteradhoc marked this conversation as resolved.
Outdated
echo '<div class="notice notice-warning"><p>' . 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' ) . '</p></div>';
}
}

// 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' ) ) {
Expand Down Expand Up @@ -86,6 +103,34 @@ public static function render_settings_page() {
echo '</tr>';
}

echo '</tbody></table>';

echo '</fieldset>';

// --- Enforcement section ---
$saved_enforced_roles = (array) get_option( 'two_factor_enforced_roles', array() );
$all_roles = wp_roles()->get_names();

echo '<h2>' . esc_html__( 'Two-Factor Enforcement', 'two-factor' ) . '</h2>';
echo '<p class="description">' . 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' ) . '</p>';

echo '<fieldset class="two-factor-enforcement"><legend class="screen-reader-text">' . esc_html__( 'Enforced Roles', 'two-factor' ) . '</legend>';
echo '<table class="form-table"><tbody>';

if ( empty( $all_roles ) ) {
echo '<tr><td>' . esc_html__( 'No roles found.', 'two-factor' ) . '</td></tr>';
} else {
echo '<tr><td>';
foreach ( $all_roles as $role_slug => $role_name ) {
$role_slug = sanitize_key( $role_slug );
echo '<p class="provider-item"><label for="role_' . esc_attr( $role_slug ) . '">';
echo '<input type="checkbox" name="two_factor_enforced_roles[]" id="role_' . esc_attr( $role_slug ) . '" value="' . esc_attr( $role_slug ) . '" ' . checked( in_array( $role_slug, $saved_enforced_roles, true ), true, false ) . ' /> ';
echo esc_html( translate_user_role( $role_name ) );
echo '</label></p>';
}
echo '</td></tr>';
}

echo '</tbody></table>';
echo '</fieldset>';

Expand Down
95 changes: 95 additions & 0 deletions two-factor.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,98 @@ 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 );

Comment thread
masteradhoc marked this conversation as resolved.
/**
* 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;
}

Comment thread
masteradhoc marked this conversation as resolved.
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 );
Loading