Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
23 changes: 16 additions & 7 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,22 +352,31 @@ public static function enable_dummy_method_for_debug( $methods ) {
}

/**
* Add "Settings" link to the plugin action links on the Plugins screen.
* Add Plugin and User Settings link to the plugin action links on the Plugins screen.
*
* @since 0.14.3
*
* @param string[] $links An array of plugin action links.
* @return string[] Modified array with the Settings link added.
* @return string[] Modified array with the User Settings link added.
*/
public static function add_settings_action_link( $links ) {
$settings_url = admin_url( 'profile.php#application-passwords-section' );
$settings_link = sprintf(
$plugin_settings_url = admin_url( 'options-general.php?page=two-factor-settings' );
$plugin_settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( $settings_url ),
esc_html__( 'Settings', 'two-factor' )
esc_url( $plugin_settings_url ),
esc_html__( 'Plugin Settings', 'two-factor' )
);

array_unshift( $links, $settings_link );
$user_settings_url = admin_url( 'profile.php#application-passwords-section' );
$user_settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( $user_settings_url ),
esc_html__( 'User Settings', 'two-factor' )
);

// Show plugin settings first, then user settings.
array_unshift( $links, $user_settings_link );
array_unshift( $links, $plugin_settings_link );

return $links;
}
Expand Down
9 changes: 3 additions & 6 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The Two-Factor plugin adds an extra layer of security to your WordPress login by

## Setup Instructions

**Important**: Each user must individually configure their two-factor authentication settings. There are no site-wide settings for this plugin.
**Important**: Each user must individually configure their two-factor authentication settings.

### For Individual Users

Expand All @@ -32,7 +32,7 @@ The Two-Factor plugin adds an extra layer of security to your WordPress login by

### For Site Administrators

- **No global settings**: This plugin operates on a per-user basis only. For more, see [GH#249](https://github.com/WordPress/two-factor/issues/249).
- **Plugin settings**: The plugin provides a settings page under "Settings → Two-Factor" to configure which providers should be disabled site-wide.
- **User management**: Administrators can configure 2FA for other users by editing their profiles
- **Security recommendations**: Encourage users to enable backup methods to prevent account lockouts

Expand Down Expand Up @@ -125,10 +125,6 @@ The plugin contributors and WordPress community take security bugs seriously. We

To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program.

= Why doesn't this plugin have site-wide settings? =

This plugin is designed to work on a per-user basis, allowing each user to choose their preferred authentication methods. This approach provides maximum flexibility and security. Site administrators can still configure 2FA for other users by editing their profiles. For more information, see [issue #437](https://github.com/WordPress/two-factor/issues/437).

= What if I lose access to all my authentication methods? =

If you have backup codes enabled, you can use one of those to regain access. If you don't have backup codes or have used them all, you'll need to contact your site administrator to reset your account. This is why it's important to always enable backup codes and keep them in a secure location.
Expand Down Expand Up @@ -240,3 +236,4 @@ Bumps WordPress minimum supported version to 6.3 and PHP minimum to 7.2.
= 0.9.0 =
Users are now asked to re-authenticate with their two-factor before making changes to their two-factor settings. This associates each login session with the two-factor login meta data for improved handling of that session.


101 changes: 101 additions & 0 deletions settings/class-two-factor-settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php
/**
* Admin settings UI for the Two-Factor plugin.
* Provides a site-wide settings screen for disabling individual Two-Factor providers.
*
* @since 0.16
*
* @package Two_Factor
*/

/**
* Settings screen renderer for Two-Factor.
*
* @since 0.16
*/
class Two_Factor_Settings {

/**
* Render the settings page.
* Also handles saving of settings when the form is submitted.
*
* @since 0.16
*
* @return void
*/
public static function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

// Handle save.
if ( isset( $_POST['two_factor_settings_submit'] ) ) {
check_admin_referer( 'two_factor_save_settings', 'two_factor_settings_nonce' );

$posted = isset( $_POST['two_factor_disabled_providers'] ) && is_array( $_POST['two_factor_disabled_providers'] ) ? wp_unslash( $_POST['two_factor_disabled_providers'] ) : array();

// Sanitize posted values immediately.
$posted = array_map( 'sanitize_text_field', (array) $posted );
// Remove empty values.
$disabled = array_values( array_filter( $posted, 'strlen' ) );

if ( ! empty( $disabled ) ) {
update_option( 'two_factor_disabled_providers', array_values( array_unique( $disabled ) ) );
} else {
// Empty means none disabled (all allowed).
delete_option( 'two_factor_disabled_providers' );
}

echo '<div class="updated"><p>' . esc_html__( 'Settings saved.', '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' ) ) {
$provider_instances = Two_Factor_Core::get_providers();
if ( ! is_array( $provider_instances ) ) {
$provider_instances = array();
}
}

$saved_disabled = get_option( 'two_factor_disabled_providers', array() );

echo '<div class="wrap two-factor-settings">';
echo '<h1>' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '</h1>';
echo '<h2>' . esc_html__( 'Disable Providers', 'two-factor' ) . '</h2>';
echo '<p class="description">' . esc_html__( 'Disable any Two-Factor providers you do not want available on this site. By default all providers are available.', 'two-factor' ) . '</p>';
echo '<form method="post" action="">';
wp_nonce_field( 'two_factor_save_settings', 'two_factor_settings_nonce' );

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

if ( empty( $provider_instances ) ) {
echo '<tr><td>' . esc_html__( 'No providers found.', 'two-factor' ) . '</td></tr>';
} else {
// Render a compact stacked list of provider checkboxes below the title/description.
echo '<tr>';
echo '<td>';
foreach ( $provider_instances as $provider_key => $instance ) {
$label = method_exists( $instance, 'get_label' ) ? $instance->get_label() : $provider_key;

echo '<p class="provider-item"><label for="provider_' . esc_attr( $provider_key ) . '">';
echo '<input type="checkbox" name="two_factor_disabled_providers[]" id="provider_' . esc_attr( $provider_key ) . '" value="' . esc_attr( $provider_key ) . '" ' . checked( in_array( $provider_key, (array) $saved_disabled, true ), true, false ) . ' /> ';
echo esc_html( $label );
echo '</label></p>';
}

echo '</td>';
echo '</tr>';
}

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

submit_button( __( 'Save Settings', 'two-factor' ), 'primary', 'two_factor_settings_submit' );
echo '</form>';

echo '</div>';
}

}
146 changes: 146 additions & 0 deletions two-factor.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,155 @@
*/
require_once TWO_FACTOR_DIR . 'class-two-factor-compat.php';

// Load settings UI class so the settings page can be rendered.
require_once TWO_FACTOR_DIR . 'settings/class-two-factor-settings.php';

$two_factor_compat = new Two_Factor_Compat();

Two_Factor_Core::add_hooks( $two_factor_compat );

// Delete our options and user meta during uninstall.
register_uninstall_hook( __FILE__, array( Two_Factor_Core::class, 'uninstall' ) );

/**
* Register admin menu and plugin action links.
*
* @since 0.16
*/
function two_factor_register_admin_hooks() {
if ( is_admin() ) {
add_action( 'admin_menu', 'two_factor_add_settings_page' );
}

// Load settings page assets when in admin.
// Settings assets handled inline via standard markup; no extra CSS enqueued.

/* Enforcement filters: restrict providers based on saved disabled option. */
add_filter( 'two_factor_providers', 'two_factor_filter_disabled_providers' );
add_filter( 'two_factor_enabled_providers_for_user', 'two_factor_filter_disabled_enabled_providers_for_user', 10, 2 );
}

add_action( 'init', 'two_factor_register_admin_hooks' );

/**
* Add the Two Factor settings page under Settings.
*
* @since 0.16
*/
function two_factor_add_settings_page() {
add_options_page(
__( 'Two-Factor Settings', 'two-factor' ),
__( 'Two-Factor', 'two-factor' ),
'manage_options',
'two-factor-settings',
'two_factor_render_settings_page'
);
}


/**
* Render the settings page via the settings class if available.
*
* @since 0.16
*/
function two_factor_render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

// Prefer new settings class (keeps main file small).
if ( class_exists( 'Two_Factor_Settings' ) && is_callable( array( 'Two_Factor_Settings', 'render_settings_page' ) ) ) {
Two_Factor_Settings::render_settings_page();
return;
}

// Fallback: no UI available.
echo '<div class="wrap"><h1>' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '</h1>';
echo '<p>' . esc_html__( 'Settings not available.', 'two-factor' ) . '</p></div>';
}


/**
* Helper: retrieve disabled providers option as an array of classnames.
* Empty array / missing option means none disabled (all allowed).
*
* @since 0.16
*
* @return array
*/
function two_factor_get_disabled_providers_option() {
$disabled = get_option( 'two_factor_disabled_providers', array() );
if ( empty( $disabled ) || ! is_array( $disabled ) ) {
return array();
}
return $disabled;
}


/**
* Filter the registered providers according to the saved disabled providers option.
* This filter receives the providers in the same shape as core: classname => path.
*
* @since 0.16
*/
function two_factor_filter_disabled_providers( $providers ) {
$disabled = two_factor_get_disabled_providers_option();

// Empty disabled list means allow all providers.
if ( empty( $disabled ) ) {
return $providers;
}

// If we are rendering the settings page, do not filter so admins may re-enable providers.
if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) {
return $providers;
}

foreach ( $providers as $key => $path ) {
if ( in_array( $key, $disabled, true ) ) {
unset( $providers[ $key ] );
}
}

return $providers;
}


/**
* Filter the supported providers for a specific user (instances keyed by provider key).
*
* @since 0.16
*/
function two_factor_filter_disabled_providers_for_user( $providers, $user ) {
$disabled = two_factor_get_disabled_providers_option();
if ( empty( $disabled ) ) {
return $providers;
}

if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) {
return $providers;
}

foreach ( $providers as $key => $instance ) {
if ( in_array( $key, $disabled, true ) ) {
unset( $providers[ $key ] );
}
}

return $providers;
}


/**
* Filter enabled providers for a user (classnames array) to enforce disabled list.
*
* @since 0.16
*/
function two_factor_filter_disabled_enabled_providers_for_user( $enabled, $user_id ) {
$disabled = two_factor_get_disabled_providers_option();
if ( empty( $disabled ) ) {
return $enabled;
}

return array_values( array_diff( (array) $enabled, $disabled ) );
}