diff --git a/class-two-factor-core.php b/class-two-factor-core.php
index 8afd98eb..a52436b3 100644
--- a/class-two-factor-core.php
+++ b/class-two-factor-core.php
@@ -643,6 +643,29 @@ public static function fetch_user( $user = null ) {
return $user;
}
+ /**
+ * Get the provider keys stored in user meta, normalised.
+ *
+ * Returns the raw stored list without intersecting against registered providers and
+ * without applying `two_factor_enabled_providers_for_user`, so callers can distinguish
+ * "this user has no registered providers left" from "a filter intentionally cleared the list".
+ *
+ * @since 0.17.0
+ *
+ * @param WP_User $user User object.
+ *
+ * @return string[] Provider keys stored for the user. May include keys that are no longer registered.
+ */
+ private static function get_stored_provider_keys_for_user( $user ) {
+ $stored = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true );
+
+ if ( ! is_array( $stored ) ) {
+ $stored = array();
+ }
+
+ return array_values( array_filter( $stored, 'is_string' ) );
+ }
+
/**
* Get two-factor providers that are enabled for the specified (or current) user
* but might not be configured, yet.
@@ -663,11 +686,10 @@ public static function get_enabled_providers_for_user( $user = null ) {
}
$providers = self::get_supported_providers_for_user( $user );
- $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true );
- if ( empty( $enabled_providers ) ) {
- $enabled_providers = array();
- }
- $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
+ $enabled_providers = array_intersect(
+ self::get_stored_provider_keys_for_user( $user ),
+ array_keys( $providers )
+ );
/**
* Filter the enabled two-factor authentication providers for this user.
@@ -690,7 +712,10 @@ public static function get_enabled_providers_for_user( $user = null ) {
* @see Two_Factor_Core::get_enabled_providers_for_user()
*
* @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user.
- * @return Two_Factor_Provider[]|WP_Error List of provider instances, or a WP_Error if all configured providers are unavailable.
+ * @return Two_Factor_Provider[]|WP_Error List of provider instances, or a WP_Error if the user's stored
+ * providers are no longer registered and the fallback provider
+ * (`Two_Factor_Email` by default, see `two_factor_fallback_provider_for_user`)
+ * doesn't resolve to a registered, available provider.
*/
public static function get_available_providers_for_user( $user = null ) {
$user = self::fetch_user( $user );
@@ -701,29 +726,74 @@ public static function get_available_providers_for_user( $user = null ) {
$providers = self::get_supported_providers_for_user( $user ); // Returns full objects.
$enabled_providers = self::get_enabled_providers_for_user( $user ); // Returns just the keys.
$configured_providers = array();
- $user_providers_raw = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true );
+ $stored_providers = self::get_stored_provider_keys_for_user( $user );
/**
- * If the user had enabled providers, but none of them exist currently,
- * if emailed codes is available force it to be on, so that deprecated
- * or removed providers don't result in the two-factor requirement being
- * removed and 'failing open'.
+ * If the user has providers stored in meta but none of them are still registered, force
+ * emailed codes on where available so removed or deprecated providers can't drop the user
+ * to single-factor auth ('failing open').
*
- * Possible enhancement: add a filter to change the fallback method?
+ * "No longer registered" is deliberately cause-agnostic: a provider dropped by plugin
+ * deactivation, by the site-wide settings, or by `two_factor_providers_for_user` is treated
+ * identically, because the outcome for the user is identical.
+ *
+ * If any stored provider IS still registered, an empty enabled list means
+ * `two_factor_enabled_providers_for_user` cleared it on purpose, and that must be respected.
*/
- if ( empty( $enabled_providers ) && $user_providers_raw ) {
- if ( isset( $providers['Two_Factor_Email'] ) ) {
- // Force Emailed codes to 'on'.
- $enabled_providers[] = 'Two_Factor_Email';
- } else {
- return new WP_Error(
- 'no_available_2fa_methods',
- __( 'Error: You have Two Factor method(s) enabled, but the provider(s) no longer exist. Please contact a site administrator for assistance.', 'two-factor' ),
- array(
- 'user_providers_raw' => $user_providers_raw,
- 'available_providers' => array_keys( $providers ),
- )
+ if ( empty( $enabled_providers ) && ! empty( $stored_providers ) ) {
+ $still_registered = array_intersect( $stored_providers, array_keys( $providers ) );
+
+ if ( empty( $still_registered ) ) {
+ /**
+ * Filter the provider forced on when none of a user's stored providers are still registered.
+ *
+ * Returning a key that is not registered, or that the provider itself reports as unavailable
+ * for this user, is treated as "no fallback": the method returns a `no_available_2fa_methods`
+ * WP_Error rather than allowing the user through with one factor.
+ *
+ * The returned provider must be usable without any prior per-user setup (like the email
+ * provider is), since the user has no working provider left to configure it through:
+ *
+ * add_filter( 'two_factor_fallback_provider_for_user', function() {
+ * return 'Two_Factor_Backup_Codes'; // Wrong: requires codes to already be generated.
+ * } );
+ *
+ * A fallback that is not already available for the user resolves to the WP_Error branch,
+ * not to a silent single-factor login.
+ *
+ * @since 0.17.0
+ *
+ * @param string $fallback_provider Provider key to force on. Default 'Two_Factor_Email'.
+ * @param int $user_id The user ID.
+ * @param string[] $stored_providers Provider keys stored for the user, none of which are registered.
+ */
+ $fallback_provider = apply_filters(
+ 'two_factor_fallback_provider_for_user',
+ 'Two_Factor_Email',
+ $user->ID,
+ $stored_providers
);
+
+ if (
+ is_string( $fallback_provider )
+ && isset( $providers[ $fallback_provider ] )
+ && $providers[ $fallback_provider ]->is_available_for_user( $user )
+ ) {
+ // Force the fallback provider to 'on'.
+ $enabled_providers[] = $fallback_provider;
+ } else {
+ // Fail closed: an invalid, unregistered, or unavailable fallback locks the user
+ // out pending admin intervention, rather than letting them through with one factor.
+ return new WP_Error(
+ 'no_available_2fa_methods',
+ __( 'Error: You have Two Factor method(s) enabled, but the provider(s) no longer exist. Please contact a site administrator for assistance.', 'two-factor' ),
+ array(
+ 'user_providers_raw' => $stored_providers,
+ 'available_providers' => array_keys( $providers ),
+ 'fallback_provider' => $fallback_provider,
+ )
+ );
+ }
}
}
diff --git a/readme.txt b/readme.txt
index 31337be9..0656ba21 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,273 +1,273 @@
-=== Two Factor ===
-Contributors: georgestephanis, kasparsd, masteradhoc, valendesigns, stevenkword, jeffpaul, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, alihusnainarshad, passoniate
-Tags: 2fa, mfa, totp, authentication, security
-Tested up to: 7.1
-Stable tag: 0.16.0
-License: GPL-2.0-or-later
-License URI: https://spdx.org/licenses/GPL-2.0-or-later.html
-
-Enable Two-Factor Authentication (2FA) using time-based one-time passwords (TOTP), email, and backup verification codes.
-
-== Description ==
-
-The Two-Factor plugin adds an extra layer of security to your WordPress login by requiring users to provide a second form of authentication in addition to their password. This helps protect against unauthorized access even if passwords are compromised.
-
-## Setup Instructions
-
-**Important**: Each user must individually configure their two-factor authentication settings.
-
-### For Individual Users
-
-1. **Navigate to your profile**: Go to "Users" → "Your Profile" in the WordPress admin
-2. **Find Two-Factor Options**: Scroll down to the "Two-Factor Options" section
-3. **Choose your methods**: Enable one or more authentication providers (noting a site admin may have hidden one or more so what is available could vary):
- - **Authenticator App (TOTP)** - Use apps like Google Authenticator, Authy, or 1Password
- - **Email Codes** - Receive one-time codes via email
- - **Backup Codes** - Generate one-time backup codes for emergencies
- - **Dummy Method** - For testing purposes only (requires WP_DEBUG)
-4. **Configure each method**: Follow the setup instructions for each enabled provider
-5. **Set primary method**: Choose which method to use as your default authentication
-6. **Save changes**: Click "Update Profile" to save your settings
-
-### For Site Administrators
-
-- **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
-
-## Available Authentication Methods
-
-### Authenticator App (TOTP) - Recommended
-- **Security**: High - Time-based one-time passwords
-- **Setup**: Scan QR code with authenticator app
-- **Compatibility**: Works with Google Authenticator, Authy, 1Password, and other TOTP apps
-- **Best for**: Most users, provides excellent security with good usability
-
-### Backup Codes - Recommended
-- **Security**: Medium - One-time use codes
-- **Setup**: Generate 10 backup codes for emergency access
-- **Compatibility**: Works everywhere, no special hardware needed
-- **Best for**: Emergency access when other methods are unavailable
-
-### Email Codes
-- **Security**: Medium - One-time codes sent via email
-- **Setup**: Automatic - uses your WordPress email address
-- **Compatibility**: Works with any email-capable device
-- **Best for**: Users who prefer email-based authentication
-
-### FIDO U2F Security Keys
-- Deprecated and removed due to loss of browser support.
-
-### Dummy Method
-- **Security**: None - Always succeeds
-- **Setup**: Only available when WP_DEBUG is enabled
-- **Purpose**: Testing and development only
-- **Best for**: Developers testing the plugin
-
-## Important Notes
-
-### HTTPS Requirement
-- All methods work on both HTTP and HTTPS sites
-
-### Browser Compatibility
-- TOTP and email methods work on all devices and browsers
-
-### Account Recovery
-- Always enable backup codes to prevent being locked out of your account
-- If you lose access to all authentication methods, contact your site administrator
-
-### Security Best Practices
-- Use multiple authentication methods when possible
-- Keep backup codes in a secure location
-- Regularly review and update your authentication settings
-
-For more information about two-factor authentication in WordPress, see the [WordPress Advanced Administration Security Guide](https://developer.wordpress.org/advanced-administration/security/mfa/).
-
-For more history, see [this post](https://georgestephanis.wordpress.com/2013/08/14/two-cents-on-two-factor/).
-
-= Actions & Filters =
-
-Here is a list of action and filter hooks provided by the plugin:
-
-- `two_factor_providers` filter overrides the available two-factor providers such as email and time-based one-time passwords. Array values are PHP classnames of the two-factor providers.
-- `two_factor_providers_for_user` filter overrides the available two-factor providers for a specific user. Array values are instances of provider classes and the user object `WP_User` is available as the second argument.
-- `two_factor_enabled_providers_for_user` filter overrides the list of two-factor providers enabled for a user. First argument is an array of enabled provider classnames as values, the second argument is the user ID.
-- `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow.
-- `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_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.
-- `two_factor_after_authentication_prompt` action which receives the provider object and fires after the prompt shown on the authentication input form.
-- `two_factor_after_authentication_input` action which receives the provider object and fires after the input shown on the authentication input form (if form contains no input, action fires immediately after `two_factor_after_authentication_prompt`).
-- `two_factor_login_backup_links` filters the backup links displayed on the two-factor login form.
-- `two_factor_login_nonce_failed` action which fires when a login nonce fails verification. Provides the ID of the user the nonce was presented for as the first argument, and the reason as the second: `no_nonce_stored`, `expired`, or `mismatch`.
-- `two_factor_log_login_nonce_failures` filter overrides whether a failed login nonce verification is written to the PHP error log. Defaults to true for `expired` and `mismatch`, and false for `no_nonce_stored`, which any unauthenticated request can reach. Provides the user ID as the second argument and the reason as the third.
-
-== Redirect After the Two-Factor Challenge ==
-
-To redirect users to a specific URL after completing the two-factor challenge, use WordPress Core built-in login_redirect filter. The filter works the same way as in a standard WordPress login flow:
-
- add_filter( 'login_redirect', function( $redirect_to, $requested_redirect_to, $user ) {
- return home_url( '/dashboard/' );
- }, 10, 3 );
-
-== Frequently Asked Questions ==
-
-= What PHP and WordPress versions does the Two-Factor plugin support? =
-
-This plugin supports the last two major versions of WordPress and the minimum PHP version supported by those WordPress versions.
-
-= How can I send feedback or get help with a bug? =
-
-The best place to report bugs, feature suggestions, or any other (non-security) feedback is at the Two Factor GitHub issues page. Before submitting a new issue, please search the existing issues to check if someone else has reported the same feedback.
-
-= Where can I report security bugs? =
-
-The plugin contributors and WordPress community take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
-
-To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program.
-
-= 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.
-
-= Can I use this plugin with WebAuthn? =
-
-The plugin previously supported FIDO U2F, which was a predecessor to WebAuthn. There is an open issue to [add WebAuthn support here](https://github.com/WordPress/two-factor/pull/427).
-
-= Is there a recommended way to use passkeys or hardware security keys with Two-Factor? =
-
-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 _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).
-
-
-== Screenshots ==
-
-1. Two-factor options under User Profile - Shows the main configuration area where users can enable different authentication methods.
-2. Email Code Authentication during WordPress Login - Shows the email verification screen that appears during login.
-3. Authenticator App (TOTP) setup with QR code - Demonstrates the QR code generation and manual key entry for TOTP setup.
-4. Backup codes generation and management - Shows the backup codes interface for generating and managing emergency access codes.
-
-== Changelog ==
-
-= 0.16.0 - 2026-03-27 =
-
-* **Breaking Changes:** Remove legacy FIDO U2F provider support by [#439](https://github.com/WordPress/two-factor/pull/439).
-* **New Features:** Add a dedicated settings page for plugin configuration in wp-admin by [#764](https://github.com/WordPress/two-factor/pull/764).
-* **New Features:** Add a support links filter so consumers can customize contextual recovery/help links by [#615](https://github.com/WordPress/two-factor/pull/615).
-* **New Features:** Refresh backup codes UI styling and behavior by [#804](https://github.com/WordPress/two-factor/pull/804).
-* **Bug Fixes:** Delete stored TOTP secrets when the TOTP provider is disabled by [#802](https://github.com/WordPress/two-factor/pull/802).
-* **Bug Fixes:** Harden provider handling so login/settings checks do not fail open when expected providers disappear by [#586](https://github.com/WordPress/two-factor/pull/586).
-* **Bug Fixes:** Ensure only configured providers are saved and enabled in user settings by [#798](https://github.com/WordPress/two-factor/pull/798).
-* **Bug Fixes:** Improve settings-page accessibility and fix profile settings link behavior by [#828](https://github.com/WordPress/two-factor/pull/828) and [#830](https://github.com/WordPress/two-factor/pull/830).
-* **Bug Fixes:** Resolve PHPCS violations in provider files by [#851](https://github.com/WordPress/two-factor/pull/851).
-* **Development Updates:** Move login styles and provider scripts from inline output to enqueued/external assets by [#807](https://github.com/WordPress/two-factor/pull/807) and [#814](https://github.com/WordPress/two-factor/pull/814).
-* **Development Updates:** Improve inline docs and static-analysis compatibility (WPCS/phpstan) by [#810](https://github.com/WordPress/two-factor/pull/810), [#815](https://github.com/WordPress/two-factor/pull/815), and [#817](https://github.com/WordPress/two-factor/pull/817).
-* **Development Updates:** Improve unit test reliability and integrate CI code coverage reporting by [#825](https://github.com/WordPress/two-factor/pull/825), [#841](https://github.com/WordPress/two-factor/pull/841), and [#842](https://github.com/WordPress/two-factor/pull/842).
-* **Development Updates:** Update readme docs and modernize CI workflow infrastructure by [#835](https://github.com/WordPress/two-factor/pull/835), [#837](https://github.com/WordPress/two-factor/pull/837), [#843](https://github.com/WordPress/two-factor/pull/843), and [#849](https://github.com/WordPress/two-factor/pull/849).
-* **Dependency Updates:** Bump `qs` from 6.14.1 to 6.14.2 by [#794](https://github.com/WordPress/two-factor/pull/794).
-* **Dependency Updates:** Bump `basic-ftp` from 5.0.5 to 5.2.0 by [#816](https://github.com/WordPress/two-factor/pull/816).
-* **Dependency Updates:** Apply automatic lint/format updates and associated Composer package refreshes by [#799](https://github.com/WordPress/two-factor/pull/799).
-
-= 0.15.0 - 2026-02-13 =
-
-* **Breaking Changes:** Trigger two-factor flow only when expected by @kasparsd in [#660](https://github.com/WordPress/two-factor/pull/660) and [#793](https://github.com/WordPress/two-factor/pull/793).
-* **New Features:** Include user IP address and contextual warning in two-factor code emails by @todeveni in [#728](https://github.com/WordPress/two-factor/pull/728)
-* **New Features:** Optimize email text for TOTP by @masteradhoc in [#789](https://github.com/WordPress/two-factor/pull/789)
-* **New Features:** Add "Settings" action link to plugin list for quick access to profile by @hardikRathi in [#740](https://github.com/WordPress/two-factor/pull/740)
-* **New Features:** Additional form hooks by @eric-michel in [#742](https://github.com/WordPress/two-factor/pull/742)
-* **New Features:** Full RFC6238 Compatibility by @ericmann in [#656](https://github.com/WordPress/two-factor/pull/656)
-* **New Features:** Consistent user experience for TOTP setup by @kasparsd in [#792](https://github.com/WordPress/two-factor/pull/792)
-* **Documentation:** `@since` docs by @masteradhoc in [#781](https://github.com/WordPress/two-factor/pull/781)
-* **Documentation:** Update user and admin docs, prepare for more screenshots by @jeffpaul in [#701](https://github.com/WordPress/two-factor/pull/701)
-* **Documentation:** Add changelog & credits, update release notes by @jeffpaul in [#696](https://github.com/WordPress/two-factor/pull/696)
-* **Documentation:** Clear readme.txt by @masteradhoc in [#785](https://github.com/WordPress/two-factor/pull/785)
-* **Documentation:** Add date and time information above TOTP setup instructions by @masteradhoc in [#772](https://github.com/WordPress/two-factor/pull/772)
-* **Documentation:** Clarify TOTP setup instructions by @masteradhoc in [#763](https://github.com/WordPress/two-factor/pull/763)
-* **Documentation:** Update RELEASING.md by @jeffpaul in [#787](https://github.com/WordPress/two-factor/pull/787)
-* **Development Updates:** Pause deploys to SVN trunk for merges to `master` by @kasparsd in [#738](https://github.com/WordPress/two-factor/pull/738)
-* **Development Updates:** Fix CI checks for PHP compatability by @kasparsd in [#739](https://github.com/WordPress/two-factor/pull/739)
-* **Development Updates:** Fix Playground refs by @kasparsd in [#744](https://github.com/WordPress/two-factor/pull/744)
-* **Development Updates:** Persist existing translations when introducing new helper text in emails by @kasparsd in [#745](https://github.com/WordPress/two-factor/pull/745)
-* **Development Updates:** Fix `missing_direct_file_access_protection` by @masteradhoc in [#760](https://github.com/WordPress/two-factor/pull/760)
-* **Development Updates:** Fix `mismatched_plugin_name` by @masteradhoc in [#754](https://github.com/WordPress/two-factor/pull/754)
-* **Development Updates:** Introduce Props Bot workflow by @jeffpaul in [#749](https://github.com/WordPress/two-factor/pull/749)
-* **Development Updates:** Plugin Check: Fix Missing $domain parameter by @masteradhoc in [#753](https://github.com/WordPress/two-factor/pull/753)
-* **Development Updates:** Tests: Update to supported WP version 6.8 by @masteradhoc in [#770](https://github.com/WordPress/two-factor/pull/770)
-* **Development Updates:** Fix PHP 8.5 deprecated message by @masteradhoc in [#762](https://github.com/WordPress/two-factor/pull/762)
-* **Development Updates:** Exclude 7.2 and 7.3 checks against trunk by @masteradhoc in [#769](https://github.com/WordPress/two-factor/pull/769)
-* **Development Updates:** Fix Plugin Check errors: `MissingTranslatorsComment` & `MissingSingularPlaceholder` by @masteradhoc in [#758](https://github.com/WordPress/two-factor/pull/758)
-* **Development Updates:** Add PHP 8.5 tests for latest and trunk version of WP by @masteradhoc in [#771](https://github.com/WordPress/two-factor/pull/771)
-* **Development Updates:** Add `phpcs:ignore` for falsepositives by @masteradhoc in [#777](https://github.com/WordPress/two-factor/pull/777)
-* **Development Updates:** Fix(totp): `otpauth` link in QR code URL by @sjinks in [#784](https://github.com/WordPress/two-factor/pull/784)
-* **Development Updates:** Update deploy.yml by @masteradhoc in [#773](https://github.com/WordPress/two-factor/pull/773)
-* **Development Updates:** Update required WordPress Version by @masteradhoc in [#765](https://github.com/WordPress/two-factor/pull/765)
-* **Development Updates:** Fix: ensure execution stops after redirects by @sjinks in [#786](https://github.com/WordPress/two-factor/pull/786)
-* **Development Updates:** Fix `WordPress.Security.EscapeOutput.OutputNotEscaped` errors by @masteradhoc in [#776](https://github.com/WordPress/two-factor/pull/776)
-* **Dependency Updates:** Bump qs and express by @dependabot[bot] in [#746](https://github.com/WordPress/two-factor/pull/746)
-* **Dependency Updates:** Bump lodash from 4.17.21 to 4.17.23 by @dependabot[bot] in [#750](https://github.com/WordPress/two-factor/pull/750)
-* **Dependency Updates:** Bump lodash-es from 4.17.21 to 4.17.23 by @dependabot[bot] in [#748](https://github.com/WordPress/two-factor/pull/748)
-* **Dependency Updates:** Bump phpunit/phpunit from 8.5.44 to 8.5.52 by @dependabot[bot] in [#755](https://github.com/WordPress/two-factor/pull/755)
-* **Dependency Updates:** Bump symfony/process from 5.4.47 to 5.4.51 by @dependabot[bot] in [#756](https://github.com/WordPress/two-factor/pull/756)
-* **Dependency Updates:** Bump qs and body-parser by @dependabot[bot] in [#782](https://github.com/WordPress/two-factor/pull/782)
-* **Dependency Updates:** Bump webpack from 5.101.3 to 5.105.0 by @dependabot[bot] in [#780](https://github.com/WordPress/two-factor/pull/780)
-
-= 0.14.2 - 2025-12-11 =
-
-* **New Features:** Add filter for rest_api_can_edit_user_and_update_two_factor_options by @gutobenn in [#689](https://github.com/WordPress/two-factor/pull/689)
-* **Development Updates:** Remove Coveralls tooling and add inline coverage report by @kasparsd in [#717](https://github.com/WordPress/two-factor/pull/717)
-* **Development Updates:** Update blueprint path to pull from main branch instead of a deleted f… by @georgestephanis in [#719](https://github.com/WordPress/two-factor/pull/719)
-* **Development Updates:** Fix blueprint and wporg asset deploys by @kasparsd in [#734](https://github.com/WordPress/two-factor/pull/734)
-* **Development Updates:** Upload release only on tag releases by @kasparsd in [#735](https://github.com/WordPress/two-factor/pull/735)
-* **Development Updates:** Bump playwright and @playwright/test by @dependabot[bot] in [#721](https://github.com/WordPress/two-factor/pull/721)
-* **Development Updates:** Bump tar-fs from 3.1.0 to 3.1.1 by @dependabot[bot] in [#720](https://github.com/WordPress/two-factor/pull/720)
-* **Development Updates:** Bump node-forge from 1.3.1 to 1.3.2 by @dependabot[bot] in [#724](https://github.com/WordPress/two-factor/pull/724)
-* **Development Updates:** Bump js-yaml by @dependabot[bot] in [#725](https://github.com/WordPress/two-factor/pull/725)
-* **Development Updates:** Mark as tested with the latest WP core version by @kasparsd in [#730](https://github.com/WordPress/two-factor/pull/730)
-
-= 0.14.1 - 2025-09-05 =
-
-- Don't URI encode the TOTP url for display. by @dd32 in [#711](https://github.com/WordPress/two-factor/pull/711)
-- Removed the duplicate Security.md by @slvignesh05 in [#712](https://github.com/WordPress/two-factor/pull/712)
-- Fixed linting issues by @sudar in [#707](https://github.com/WordPress/two-factor/pull/707)
-- Update development dependencies and fix failing QR unit test by @kasparsd in [#714](https://github.com/WordPress/two-factor/pull/714)
-- Trigger checkbox js change event by @gedeminas in [#688](https://github.com/WordPress/two-factor/pull/688)
-
-= 0.14.0 - 2025-07-03 =
-
-* **Features:** Enable Application Passwords for REST API and XML-RPC authentication (by default) by @joostdekeijzer in [#697](https://github.com/WordPress/two-factor/pull/697) and [#698](https://github.com/WordPress/two-factor/pull/698). Previously this required two_factor_user_api_login_enable filter to be set to true which is now the default during application password auth. XML-RPC login is still disabled for regular user passwords.
-* **Features:** Label recommended methods to simplify the configuration by @kasparsd in [#676](https://github.com/WordPress/two-factor/pull/676) and [#675](https://github.com/WordPress/two-factor/pull/675)
-* **Documentation:** Add WP.org plugin demo by @kasparsd in [#667](https://github.com/WordPress/two-factor/pull/667)
-* **Documentation:** Document supported versions of WP core and PHP by @jeffpaul in [#695](https://github.com/WordPress/two-factor/pull/695)
-* **Documentation:** Document the release process by @jeffpaul in [#684](https://github.com/WordPress/two-factor/pull/684)
-* **Tooling:** Remove duplicate WP.org screenshots and graphics from SVN trunk by @jeffpaul in [#683](https://github.com/WordPress/two-factor/pull/683)
-
-= 0.13.0 - 2025-04-02 =
-
-- Add two_factor_providers_for_user filter to limit two-factor providers available to each user by @kasparsd in [#669](https://github.com/WordPress/two-factor/pull/669)
-- Update automated testing to cover PHP 8.4 and default to PHP 8.3 by @BrookeDot in [#665](https://github.com/WordPress/two-factor/pull/665)
-
-[View the complete changelog details here](https://github.com/wordpress/two-factor/blob/master/CHANGELOG.md).
-
-== Upgrade Notice ==
-
-= 0.10.0 =
-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.
-
-
+=== Two Factor ===
+Contributors: georgestephanis, kasparsd, masteradhoc, valendesigns, stevenkword, jeffpaul, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, alihusnainarshad, passoniate
+Tags: 2fa, mfa, totp, authentication, security
+Tested up to: 7.1
+Stable tag: 0.16.0
+License: GPL-2.0-or-later
+License URI: https://spdx.org/licenses/GPL-2.0-or-later.html
+
+Enable Two-Factor Authentication (2FA) using time-based one-time passwords (TOTP), email, and backup verification codes.
+
+== Description ==
+
+The Two-Factor plugin adds an extra layer of security to your WordPress login by requiring users to provide a second form of authentication in addition to their password. This helps protect against unauthorized access even if passwords are compromised.
+
+## Setup Instructions
+
+**Important**: Each user must individually configure their two-factor authentication settings.
+
+### For Individual Users
+
+1. **Navigate to your profile**: Go to "Users" → "Your Profile" in the WordPress admin
+2. **Find Two-Factor Options**: Scroll down to the "Two-Factor Options" section
+3. **Choose your methods**: Enable one or more authentication providers (noting a site admin may have hidden one or more so what is available could vary):
+ - **Authenticator App (TOTP)** - Use apps like Google Authenticator, Authy, or 1Password
+ - **Email Codes** - Receive one-time codes via email
+ - **Backup Codes** - Generate one-time backup codes for emergencies
+ - **Dummy Method** - For testing purposes only (requires WP_DEBUG)
+4. **Configure each method**: Follow the setup instructions for each enabled provider
+5. **Set primary method**: Choose which method to use as your default authentication
+6. **Save changes**: Click "Update Profile" to save your settings
+
+### For Site Administrators
+
+- **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
+
+## Available Authentication Methods
+
+### Authenticator App (TOTP) - Recommended
+- **Security**: High - Time-based one-time passwords
+- **Setup**: Scan QR code with authenticator app
+- **Compatibility**: Works with Google Authenticator, Authy, 1Password, and other TOTP apps
+- **Best for**: Most users, provides excellent security with good usability
+
+### Backup Codes - Recommended
+- **Security**: Medium - One-time use codes
+- **Setup**: Generate 10 backup codes for emergency access
+- **Compatibility**: Works everywhere, no special hardware needed
+- **Best for**: Emergency access when other methods are unavailable
+
+### Email Codes
+- **Security**: Medium - One-time codes sent via email
+- **Setup**: Automatic - uses your WordPress email address
+- **Compatibility**: Works with any email-capable device
+- **Best for**: Users who prefer email-based authentication
+
+### FIDO U2F Security Keys
+- Deprecated and removed due to loss of browser support.
+
+### Dummy Method
+- **Security**: None - Always succeeds
+- **Setup**: Only available when WP_DEBUG is enabled
+- **Purpose**: Testing and development only
+- **Best for**: Developers testing the plugin
+
+## Important Notes
+
+### HTTPS Requirement
+- All methods work on both HTTP and HTTPS sites
+
+### Browser Compatibility
+- TOTP and email methods work on all devices and browsers
+
+### Account Recovery
+- Always enable backup codes to prevent being locked out of your account
+- If you lose access to all authentication methods, contact your site administrator
+
+### Security Best Practices
+- Use multiple authentication methods when possible
+- Keep backup codes in a secure location
+- Regularly review and update your authentication settings
+
+For more information about two-factor authentication in WordPress, see the [WordPress Advanced Administration Security Guide](https://developer.wordpress.org/advanced-administration/security/mfa/).
+
+For more history, see [this post](https://georgestephanis.wordpress.com/2013/08/14/two-cents-on-two-factor/).
+
+= Actions & Filters =
+
+Here is a list of action and filter hooks provided by the plugin:
+
+- `two_factor_providers` filter overrides the available two-factor providers such as email and time-based one-time passwords. Array values are PHP classnames of the two-factor providers.
+- `two_factor_providers_for_user` filter overrides the available two-factor providers for a specific user. Array values are instances of provider classes and the user object `WP_User` is available as the second argument.
+- `two_factor_enabled_providers_for_user` filter overrides the list of two-factor providers enabled for a user. First argument is an array of enabled provider classnames as values, the second argument is the user ID.
+- `two_factor_fallback_provider_for_user` filter overrides the provider forced on when none of a user's stored two-factor providers are still registered (e.g. after a provider plugin is deactivated). Defaults to `Two_Factor_Email`. First argument is the provider classname, the second is the user ID, the third is the array of provider classnames that were stored for the user but are no longer registered. The returned provider must be registered and available to the user (`is_available_for_user()`), or the user is shown an error instead of being let through with a fallback.
+- `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow.
+- `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_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.
+- `two_factor_after_authentication_prompt` action which receives the provider object and fires after the prompt shown on the authentication input form.
+- `two_factor_after_authentication_input` action which receives the provider object and fires after the input shown on the authentication input form (if form contains no input, action fires immediately after `two_factor_after_authentication_prompt`).
+- `two_factor_login_backup_links` filters the backup links displayed on the two-factor login form.
+- `two_factor_login_nonce_failed` action which fires when a login nonce fails verification. Provides the ID of the user the nonce was presented for as the first argument, and the reason as the second: `no_nonce_stored`, `expired`, or `mismatch`.
+- `two_factor_log_login_nonce_failures` filter overrides whether a failed login nonce verification is written to the PHP error log. Defaults to true for `expired` and `mismatch`, and false for `no_nonce_stored`, which any unauthenticated request can reach. Provides the user ID as the second argument and the reason as the third.
+
+== Redirect After the Two-Factor Challenge ==
+
+To redirect users to a specific URL after completing the two-factor challenge, use WordPress Core built-in login_redirect filter. The filter works the same way as in a standard WordPress login flow:
+
+ add_filter( 'login_redirect', function( $redirect_to, $requested_redirect_to, $user ) {
+ return home_url( '/dashboard/' );
+ }, 10, 3 );
+
+== Frequently Asked Questions ==
+
+= What PHP and WordPress versions does the Two-Factor plugin support? =
+
+This plugin supports the last two major versions of WordPress and the minimum PHP version supported by those WordPress versions.
+
+= How can I send feedback or get help with a bug? =
+
+The best place to report bugs, feature suggestions, or any other (non-security) feedback is at the Two Factor GitHub issues page. Before submitting a new issue, please search the existing issues to check if someone else has reported the same feedback.
+
+= Where can I report security bugs? =
+
+The plugin contributors and WordPress community take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
+
+To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program.
+
+= 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.
+
+= Can I use this plugin with WebAuthn? =
+
+The plugin previously supported FIDO U2F, which was a predecessor to WebAuthn. There is an open issue to [add WebAuthn support here](https://github.com/WordPress/two-factor/pull/427).
+
+= Is there a recommended way to use passkeys or hardware security keys with Two-Factor? =
+
+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 _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).
+
+
+== Screenshots ==
+
+1. Two-factor options under User Profile - Shows the main configuration area where users can enable different authentication methods.
+2. Email Code Authentication during WordPress Login - Shows the email verification screen that appears during login.
+3. Authenticator App (TOTP) setup with QR code - Demonstrates the QR code generation and manual key entry for TOTP setup.
+4. Backup codes generation and management - Shows the backup codes interface for generating and managing emergency access codes.
+
+== Changelog ==
+
+= 0.16.0 - 2026-03-27 =
+
+* **Breaking Changes:** Remove legacy FIDO U2F provider support by [#439](https://github.com/WordPress/two-factor/pull/439).
+* **New Features:** Add a dedicated settings page for plugin configuration in wp-admin by [#764](https://github.com/WordPress/two-factor/pull/764).
+* **New Features:** Add a support links filter so consumers can customize contextual recovery/help links by [#615](https://github.com/WordPress/two-factor/pull/615).
+* **New Features:** Refresh backup codes UI styling and behavior by [#804](https://github.com/WordPress/two-factor/pull/804).
+* **Bug Fixes:** Delete stored TOTP secrets when the TOTP provider is disabled by [#802](https://github.com/WordPress/two-factor/pull/802).
+* **Bug Fixes:** Harden provider handling so login/settings checks do not fail open when expected providers disappear by [#586](https://github.com/WordPress/two-factor/pull/586).
+* **Bug Fixes:** Ensure only configured providers are saved and enabled in user settings by [#798](https://github.com/WordPress/two-factor/pull/798).
+* **Bug Fixes:** Improve settings-page accessibility and fix profile settings link behavior by [#828](https://github.com/WordPress/two-factor/pull/828) and [#830](https://github.com/WordPress/two-factor/pull/830).
+* **Bug Fixes:** Resolve PHPCS violations in provider files by [#851](https://github.com/WordPress/two-factor/pull/851).
+* **Development Updates:** Move login styles and provider scripts from inline output to enqueued/external assets by [#807](https://github.com/WordPress/two-factor/pull/807) and [#814](https://github.com/WordPress/two-factor/pull/814).
+* **Development Updates:** Improve inline docs and static-analysis compatibility (WPCS/phpstan) by [#810](https://github.com/WordPress/two-factor/pull/810), [#815](https://github.com/WordPress/two-factor/pull/815), and [#817](https://github.com/WordPress/two-factor/pull/817).
+* **Development Updates:** Improve unit test reliability and integrate CI code coverage reporting by [#825](https://github.com/WordPress/two-factor/pull/825), [#841](https://github.com/WordPress/two-factor/pull/841), and [#842](https://github.com/WordPress/two-factor/pull/842).
+* **Development Updates:** Update readme docs and modernize CI workflow infrastructure by [#835](https://github.com/WordPress/two-factor/pull/835), [#837](https://github.com/WordPress/two-factor/pull/837), [#843](https://github.com/WordPress/two-factor/pull/843), and [#849](https://github.com/WordPress/two-factor/pull/849).
+* **Dependency Updates:** Bump `qs` from 6.14.1 to 6.14.2 by [#794](https://github.com/WordPress/two-factor/pull/794).
+* **Dependency Updates:** Bump `basic-ftp` from 5.0.5 to 5.2.0 by [#816](https://github.com/WordPress/two-factor/pull/816).
+* **Dependency Updates:** Apply automatic lint/format updates and associated Composer package refreshes by [#799](https://github.com/WordPress/two-factor/pull/799).
+
+= 0.15.0 - 2026-02-13 =
+
+* **Breaking Changes:** Trigger two-factor flow only when expected by @kasparsd in [#660](https://github.com/WordPress/two-factor/pull/660) and [#793](https://github.com/WordPress/two-factor/pull/793).
+* **New Features:** Include user IP address and contextual warning in two-factor code emails by @todeveni in [#728](https://github.com/WordPress/two-factor/pull/728)
+* **New Features:** Optimize email text for TOTP by @masteradhoc in [#789](https://github.com/WordPress/two-factor/pull/789)
+* **New Features:** Add "Settings" action link to plugin list for quick access to profile by @hardikRathi in [#740](https://github.com/WordPress/two-factor/pull/740)
+* **New Features:** Additional form hooks by @eric-michel in [#742](https://github.com/WordPress/two-factor/pull/742)
+* **New Features:** Full RFC6238 Compatibility by @ericmann in [#656](https://github.com/WordPress/two-factor/pull/656)
+* **New Features:** Consistent user experience for TOTP setup by @kasparsd in [#792](https://github.com/WordPress/two-factor/pull/792)
+* **Documentation:** `@since` docs by @masteradhoc in [#781](https://github.com/WordPress/two-factor/pull/781)
+* **Documentation:** Update user and admin docs, prepare for more screenshots by @jeffpaul in [#701](https://github.com/WordPress/two-factor/pull/701)
+* **Documentation:** Add changelog & credits, update release notes by @jeffpaul in [#696](https://github.com/WordPress/two-factor/pull/696)
+* **Documentation:** Clear readme.txt by @masteradhoc in [#785](https://github.com/WordPress/two-factor/pull/785)
+* **Documentation:** Add date and time information above TOTP setup instructions by @masteradhoc in [#772](https://github.com/WordPress/two-factor/pull/772)
+* **Documentation:** Clarify TOTP setup instructions by @masteradhoc in [#763](https://github.com/WordPress/two-factor/pull/763)
+* **Documentation:** Update RELEASING.md by @jeffpaul in [#787](https://github.com/WordPress/two-factor/pull/787)
+* **Development Updates:** Pause deploys to SVN trunk for merges to `master` by @kasparsd in [#738](https://github.com/WordPress/two-factor/pull/738)
+* **Development Updates:** Fix CI checks for PHP compatability by @kasparsd in [#739](https://github.com/WordPress/two-factor/pull/739)
+* **Development Updates:** Fix Playground refs by @kasparsd in [#744](https://github.com/WordPress/two-factor/pull/744)
+* **Development Updates:** Persist existing translations when introducing new helper text in emails by @kasparsd in [#745](https://github.com/WordPress/two-factor/pull/745)
+* **Development Updates:** Fix `missing_direct_file_access_protection` by @masteradhoc in [#760](https://github.com/WordPress/two-factor/pull/760)
+* **Development Updates:** Fix `mismatched_plugin_name` by @masteradhoc in [#754](https://github.com/WordPress/two-factor/pull/754)
+* **Development Updates:** Introduce Props Bot workflow by @jeffpaul in [#749](https://github.com/WordPress/two-factor/pull/749)
+* **Development Updates:** Plugin Check: Fix Missing $domain parameter by @masteradhoc in [#753](https://github.com/WordPress/two-factor/pull/753)
+* **Development Updates:** Tests: Update to supported WP version 6.8 by @masteradhoc in [#770](https://github.com/WordPress/two-factor/pull/770)
+* **Development Updates:** Fix PHP 8.5 deprecated message by @masteradhoc in [#762](https://github.com/WordPress/two-factor/pull/762)
+* **Development Updates:** Exclude 7.2 and 7.3 checks against trunk by @masteradhoc in [#769](https://github.com/WordPress/two-factor/pull/769)
+* **Development Updates:** Fix Plugin Check errors: `MissingTranslatorsComment` & `MissingSingularPlaceholder` by @masteradhoc in [#758](https://github.com/WordPress/two-factor/pull/758)
+* **Development Updates:** Add PHP 8.5 tests for latest and trunk version of WP by @masteradhoc in [#771](https://github.com/WordPress/two-factor/pull/771)
+* **Development Updates:** Add `phpcs:ignore` for falsepositives by @masteradhoc in [#777](https://github.com/WordPress/two-factor/pull/777)
+* **Development Updates:** Fix(totp): `otpauth` link in QR code URL by @sjinks in [#784](https://github.com/WordPress/two-factor/pull/784)
+* **Development Updates:** Update deploy.yml by @masteradhoc in [#773](https://github.com/WordPress/two-factor/pull/773)
+* **Development Updates:** Update required WordPress Version by @masteradhoc in [#765](https://github.com/WordPress/two-factor/pull/765)
+* **Development Updates:** Fix: ensure execution stops after redirects by @sjinks in [#786](https://github.com/WordPress/two-factor/pull/786)
+* **Development Updates:** Fix `WordPress.Security.EscapeOutput.OutputNotEscaped` errors by @masteradhoc in [#776](https://github.com/WordPress/two-factor/pull/776)
+* **Dependency Updates:** Bump qs and express by @dependabot[bot] in [#746](https://github.com/WordPress/two-factor/pull/746)
+* **Dependency Updates:** Bump lodash from 4.17.21 to 4.17.23 by @dependabot[bot] in [#750](https://github.com/WordPress/two-factor/pull/750)
+* **Dependency Updates:** Bump lodash-es from 4.17.21 to 4.17.23 by @dependabot[bot] in [#748](https://github.com/WordPress/two-factor/pull/748)
+* **Dependency Updates:** Bump phpunit/phpunit from 8.5.44 to 8.5.52 by @dependabot[bot] in [#755](https://github.com/WordPress/two-factor/pull/755)
+* **Dependency Updates:** Bump symfony/process from 5.4.47 to 5.4.51 by @dependabot[bot] in [#756](https://github.com/WordPress/two-factor/pull/756)
+* **Dependency Updates:** Bump qs and body-parser by @dependabot[bot] in [#782](https://github.com/WordPress/two-factor/pull/782)
+* **Dependency Updates:** Bump webpack from 5.101.3 to 5.105.0 by @dependabot[bot] in [#780](https://github.com/WordPress/two-factor/pull/780)
+
+= 0.14.2 - 2025-12-11 =
+
+* **New Features:** Add filter for rest_api_can_edit_user_and_update_two_factor_options by @gutobenn in [#689](https://github.com/WordPress/two-factor/pull/689)
+* **Development Updates:** Remove Coveralls tooling and add inline coverage report by @kasparsd in [#717](https://github.com/WordPress/two-factor/pull/717)
+* **Development Updates:** Update blueprint path to pull from main branch instead of a deleted f… by @georgestephanis in [#719](https://github.com/WordPress/two-factor/pull/719)
+* **Development Updates:** Fix blueprint and wporg asset deploys by @kasparsd in [#734](https://github.com/WordPress/two-factor/pull/734)
+* **Development Updates:** Upload release only on tag releases by @kasparsd in [#735](https://github.com/WordPress/two-factor/pull/735)
+* **Development Updates:** Bump playwright and @playwright/test by @dependabot[bot] in [#721](https://github.com/WordPress/two-factor/pull/721)
+* **Development Updates:** Bump tar-fs from 3.1.0 to 3.1.1 by @dependabot[bot] in [#720](https://github.com/WordPress/two-factor/pull/720)
+* **Development Updates:** Bump node-forge from 1.3.1 to 1.3.2 by @dependabot[bot] in [#724](https://github.com/WordPress/two-factor/pull/724)
+* **Development Updates:** Bump js-yaml by @dependabot[bot] in [#725](https://github.com/WordPress/two-factor/pull/725)
+* **Development Updates:** Mark as tested with the latest WP core version by @kasparsd in [#730](https://github.com/WordPress/two-factor/pull/730)
+
+= 0.14.1 - 2025-09-05 =
+
+- Don't URI encode the TOTP url for display. by @dd32 in [#711](https://github.com/WordPress/two-factor/pull/711)
+- Removed the duplicate Security.md by @slvignesh05 in [#712](https://github.com/WordPress/two-factor/pull/712)
+- Fixed linting issues by @sudar in [#707](https://github.com/WordPress/two-factor/pull/707)
+- Update development dependencies and fix failing QR unit test by @kasparsd in [#714](https://github.com/WordPress/two-factor/pull/714)
+- Trigger checkbox js change event by @gedeminas in [#688](https://github.com/WordPress/two-factor/pull/688)
+
+= 0.14.0 - 2025-07-03 =
+
+* **Features:** Enable Application Passwords for REST API and XML-RPC authentication (by default) by @joostdekeijzer in [#697](https://github.com/WordPress/two-factor/pull/697) and [#698](https://github.com/WordPress/two-factor/pull/698). Previously this required two_factor_user_api_login_enable filter to be set to true which is now the default during application password auth. XML-RPC login is still disabled for regular user passwords.
+* **Features:** Label recommended methods to simplify the configuration by @kasparsd in [#676](https://github.com/WordPress/two-factor/pull/676) and [#675](https://github.com/WordPress/two-factor/pull/675)
+* **Documentation:** Add WP.org plugin demo by @kasparsd in [#667](https://github.com/WordPress/two-factor/pull/667)
+* **Documentation:** Document supported versions of WP core and PHP by @jeffpaul in [#695](https://github.com/WordPress/two-factor/pull/695)
+* **Documentation:** Document the release process by @jeffpaul in [#684](https://github.com/WordPress/two-factor/pull/684)
+* **Tooling:** Remove duplicate WP.org screenshots and graphics from SVN trunk by @jeffpaul in [#683](https://github.com/WordPress/two-factor/pull/683)
+
+= 0.13.0 - 2025-04-02 =
+
+- Add two_factor_providers_for_user filter to limit two-factor providers available to each user by @kasparsd in [#669](https://github.com/WordPress/two-factor/pull/669)
+- Update automated testing to cover PHP 8.4 and default to PHP 8.3 by @BrookeDot in [#665](https://github.com/WordPress/two-factor/pull/665)
+
+[View the complete changelog details here](https://github.com/wordpress/two-factor/blob/master/CHANGELOG.md).
+
+== Upgrade Notice ==
+
+= 0.10.0 =
+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.
+
diff --git a/tests/class-two-factor-core.php b/tests/class-two-factor-core.php
index 8af750cb..1ebeb418 100644
--- a/tests/class-two-factor-core.php
+++ b/tests/class-two-factor-core.php
@@ -1340,7 +1340,7 @@ public function test_enable_disable_provider_for_user() {
$totp_disabled = Two_Factor_Core::disable_provider_for_user( $user->ID, 'Two_Factor_Totp' );
$enabled_providers = Two_Factor_Core::get_enabled_providers_for_user( $user->ID );
$this->assertTrue( $totp_disabled, 'Can disable a provider that is enabled' );
- $this->assertSame( array( 1 => 'Two_Factor_Dummy' ), $enabled_providers, 'The other providers are kept enabled' );
+ $this->assertSame( array( 'Two_Factor_Dummy' ), $enabled_providers, 'The other providers are kept enabled' );
$this->assertSame( 'Two_Factor_Dummy', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key(), 'Primary is updated to the first available' );
}
@@ -2655,6 +2655,77 @@ public function test_get_available_providers_for_user_with_configured_providers(
$this->assertCount( 2, $available, 'Two providers are available' );
}
+ /**
+ * Ensure an intentionally emptied provider list is respected.
+ *
+ * @covers Two_Factor_Core::get_available_providers_for_user
+ */
+ public function test_get_available_providers_for_user_respects_filter_cleared_list() {
+ $user = self::factory()->user->create_and_get();
+
+ update_user_meta( $user->ID, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Email' ) );
+
+ $filter = function ( $enabled_providers, $user_id ) use ( $user ) {
+ $this->assertSame( $user->ID, $user_id, 'Filter received expected user ID' );
+ return array();
+ };
+
+ add_filter( 'two_factor_enabled_providers_for_user', $filter, 10, 2 );
+
+ try {
+ $this->assertEmpty(
+ Two_Factor_Core::get_available_providers_for_user( $user->ID ),
+ 'No fallback provider is forced when the filter intentionally returns an empty list'
+ );
+ } finally {
+ remove_filter( 'two_factor_enabled_providers_for_user', $filter, 10 );
+ }
+ }
+
+ /**
+ * Ensure fallback still applies when configured providers are no longer registered.
+ *
+ * @covers Two_Factor_Core::get_available_providers_for_user
+ */
+ public function test_get_available_providers_for_user_falls_back_when_configured_providers_are_missing() {
+ $user = self::factory()->user->create_and_get();
+
+ update_user_meta( $user->ID, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Missing' ) );
+
+ $available = Two_Factor_Core::get_available_providers_for_user( $user->ID );
+
+ $this->assertCount( 1, $available, 'Email fallback remains active when configured providers are missing' );
+ $this->assertArrayHasKey( 'Two_Factor_Email', $available, 'Emailed codes are forced on for missing configured providers' );
+ }
+
+ /**
+ * Ensure an unregistered `two_factor_fallback_provider_for_user` return value fails closed
+ * instead of silently letting the user through with no second factor.
+ *
+ * @covers Two_Factor_Core::get_available_providers_for_user
+ */
+ public function test_get_available_providers_for_user_fails_closed_on_invalid_fallback_provider() {
+ $user = self::factory()->user->create_and_get();
+
+ update_user_meta( $user->ID, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Missing' ) );
+
+ $filter = function () {
+ return 'Two_Factor_Nonexistent';
+ };
+
+ add_filter( 'two_factor_fallback_provider_for_user', $filter );
+
+ try {
+ $result = Two_Factor_Core::get_available_providers_for_user( $user->ID );
+
+ $this->assertInstanceOf( WP_Error::class, $result, 'An unregistered fallback provider results in a WP_Error' );
+ $this->assertSame( 'no_available_2fa_methods', $result->get_error_code() );
+ $this->assertSame( 'Two_Factor_Nonexistent', $result->get_error_data()['fallback_provider'], 'Error data records the rejected fallback provider' );
+ } finally {
+ remove_filter( 'two_factor_fallback_provider_for_user', $filter );
+ }
+ }
+
/**
* Verify process_provider() returns WP_Error when no provider is given.
*