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
28 changes: 26 additions & 2 deletions providers/class-two-factor-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,22 +312,46 @@ public function generate_and_email_token( $user ) {
* Filters the token email subject.
*
* @since 0.5.2
* @deprecated 0.17.0 Use {@see 'two_factor_email_token_subject'} instead.
*
* @param string $subject The email subject line.
* @param int $user_id The ID of the user.
*/
$subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID );
$subject = apply_filters_deprecated( 'two_factor_token_email_subject', array( $subject, $user->ID ), '0.17.0', 'two_factor_email_token_subject' );

/**
* Filters the token email subject.
*
* @since 0.17.0
*
* @param string $subject The email subject line.
* @param string $token The token.
* @param int $user_id The ID of the user.
*/
$subject = apply_filters( 'two_factor_email_token_subject', $subject, $token, $user->ID );

/**
* Filters the token email message.
*
* @since 0.5.2
* @deprecated 0.17.0 Use {@see 'two_factor_email_token_message'} instead.
*
* @param string $message The email message.
* @param string $token The token.
* @param int $user_id The ID of the user.
*/
$message = apply_filters_deprecated( 'two_factor_token_email_message', array( $message, $token, $user->ID ), '0.11.0', 'two_factor_email_token_message' );

/**
* Filters the token email message.
*
* @since 0.17.0
*
* @param string $message The email message.
* @param string $token The token.
* @param int $user_id The ID of the user.
*/
$message = apply_filters( 'two_factor_token_email_message', $message, $token, $user->ID );
$message = apply_filters( 'two_factor_email_token_message', $message, $token, $user->ID );

return wp_mail( $user->user_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
}
Expand Down
12 changes: 7 additions & 5 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ Here is a list of action and filter hooks provided by the plugin:
- `two_factor_user_api_login_enable` filter restricts authentication for REST API and XML-RPC to application passwords only. Provides the user ID as the second argument.
- `two_factor_email_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated.
- `two_factor_email_token_length` filter overrides the default 8 character count for email tokens.
- `two_factor_email_token_subject` filter overrides the subject of messages sent by the email provider. Accepts the subject text as the first argument, the login token as the second argument, and the user ID as the third argument.
- `two_factor_email_token_message` filter overrides the body of messages sent by the email provider. Accepts the message text as the first argument, the login token as the second argument, and the user ID as the third argument.
- `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Provides the `WP_User` of the associated user as the second argument.
- `two_factor_rest_api_can_edit_user` filter overrides whether a user’s Two-Factor settings can be edited via the REST API. First argument is the current `$can_edit` boolean, the second argument is the user ID.
- `two_factor_before_authentication_prompt` action which receives the provider object and fires prior to the prompt shown on the authentication input form.
Expand Down Expand Up @@ -140,15 +142,15 @@ The plugin previously supported FIDO U2F, which was a predecessor to WebAuthn. T
Yes. For passkeys and hardware security keys, you can install the [Two-Factor Provider: WebAuthn plugin](https://wordpress.org/plugins/two-factor-provider-webauthn/). It integrates directly with Two-Factor and adds WebAuthn-based authentication as an additional two-factor option for users.

= Does this plugin work on WordPress Multisite? =

Yes. The Two-Factor plugin is compatible with WordPress Multisite. Each user configures their own 2FA settings via their profile, and because authentication codes are stored in WordPress user meta, the configuration is tied to the user account and valid across all sites in the network. However, there are no network-wide settings — a super admin cannot enforce or configure 2FA globally from the Network Admin dashboard. To manage 2FA for a specific user, edit their profile on any site where they have an account.

= How do I disable 2FA for a user who is locked out? =

As an administrator, go to **Users → All Users** in the WordPress admin, click **Edit** on the affected user's profile, scroll down to the **Two-Factor Options** section, and uncheck all enabled methods, then click **Update User**. This will remove 2FA for that user, allowing them to log in with their password alone. You can also do this via WP-CLI with `wp user meta delete <user_id> _two_factor_enabled_providers`. Once they're back in, encourage them to re-enable 2FA and generate fresh backup codes.

= Can I require 2FA for all users or specific roles? =

Not through the plugin's interface — there are no built-in enforcement settings. However, developers can use the `two_factor_providers_for_user` filter to control which providers are available per user or role, and combine it with custom logic to redirect users who haven't set up 2FA. Native enforcement support is a known and tracked feature request — follow the discussion at [GitHub issue #255](https://github.com/WordPress/two-factor/issues/255).


Expand Down
91 changes: 91 additions & 0 deletions tests/providers/class-two-factor-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,97 @@ function () {
remove_all_filters( 'two_factor_token_ttl' );
}

/**
* Test that the token email subject can be filtered.
* Tests cover current and deprecated filters.
*
* @expectedDeprecated two_factor_token_email_subject
*/
public function test_email_token_subject_filter() {
$user = self::factory()->user->create_and_get();
$this->provider->generate_and_email_token( $user );
$default_email = end( self::$mockmailer->mock_sent );

add_filter(
'two_factor_token_email_subject',
function ( $subject ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Required to match filter signature.
return 'New Subject';
}
);

$this->provider->generate_and_email_token( $user );
$custom_email_deprecated = end( self::$mockmailer->mock_sent );

$this->assertNotEquals( $default_email['subject'], $custom_email_deprecated['subject'], 'Email subject modified by filter' );
$this->assertEquals( 'New Subject', $custom_email_deprecated['subject'], 'Email subject matches the filter value' );

remove_all_filters( 'two_factor_token_email_subject' );

add_filter(
'two_factor_email_token_subject',
function ( $subject, $token ) {
return "Your token is: {$token}!";
},
10,
2
);

$this->provider->generate_and_email_token( $user );
$custom_email = end( self::$mockmailer->mock_sent );

$this->assertNotEquals( $default_email['subject'], $custom_email['subject'], 'Email subject modified by filter' );
$this->assertMatchesRegularExpression( '/Your token is: [0-9]+!/', $custom_email['subject'], 'Email subject matches the filter value' );

remove_all_filters( 'two_factor_email_token_subject' );
}

/**
* Test that the token email message can be filtered.
* Tests cover current and deprecated filters.
*
* @expectedDeprecated two_factor_token_email_message
*/
public function test_email_token_message_filter() {
$user = self::factory()->user->create_and_get();
$this->provider->generate_and_email_token( $user );
$default_email = end( self::$mockmailer->mock_sent );

// Deprecated filter was renamed, use the same callback to test both filters.
$callback = function ( $message, $token ) {
return "<span>$token</span>";
};

add_filter(
'two_factor_token_email_message',
$callback,
10,
2
);

$this->provider->generate_and_email_token( $user );
$custom_email_deprecated = end( self::$mockmailer->mock_sent );

$this->assertNotEquals( $default_email['body'], $custom_email_deprecated['body'], 'Email message modified by filter' );
$this->assertMatchesRegularExpression( '/<span>[0-9]+<\/span>/', $custom_email_deprecated['body'], 'Email messages contains the wrapped token' );

remove_all_filters( 'two_factor_token_email_message' );

add_filter(
'two_factor_email_token_message',
$callback,
10,
2
);

$this->provider->generate_and_email_token( $user );
$custom_email = end( self::$mockmailer->mock_sent );

$this->assertNotEquals( $default_email['body'], $custom_email['body'], 'Email message modified by filter' );
$this->assertMatchesRegularExpression( '/<span>[0-9]+<\/span>/', $custom_email['body'], 'Email messages contains the wrapped token' );

remove_all_filters( 'two_factor_email_token_message' );
}

/**
* Verify the alternative provider label contains expected text.
*
Expand Down
Loading