Skip to content

Fix: respect intentional empty provider list from two_factor_enabled_providers_for_user filter#882

Open
masteradhoc wants to merge 2 commits intoWordPress:masterfrom
masteradhoc:871-improve-failopen
Open

Fix: respect intentional empty provider list from two_factor_enabled_providers_for_user filter#882
masteradhoc wants to merge 2 commits intoWordPress:masterfrom
masteradhoc:871-improve-failopen

Conversation

@masteradhoc
Copy link
Copy Markdown
Collaborator

What?

Fixes #871

Adds a new two_factor_email_fallback_enabled filter that allows the email provider fallback to be disabled when a user's provider list is intentionally emptied via the two_factor_enabled_providers_for_user filter.

Why?

Since 0.16.0, the fail-safe behaviour in get_available_providers_for_user() forces Two_Factor_Email on whenever the resolved provider list is empty but the user has providers stored in user meta. This was introduced to prevent "failing open" when providers are removed or deprecated — which is correct and should be kept.

However, it also breaks the legitimate use case of using two_factor_enabled_providers_for_user to intentionally return an empty array (e.g. to bypass 2FA for users connecting from a trusted IP address). Previously this worked; 0.16.0 made it impossible without a workaround.

How?

Before applying the email fallback, we now compute $unfiltered — the intersection of the raw user meta providers with the currently available providers. This tells us whether the provider list became empty because providers genuinely disappeared, or because a filter cleared it intentionally:

  • If $unfiltered is empty, the providers are genuinely missing/removed — the fail-safe applies unconditionally, as before.
  • If $unfiltered is not empty, the filter cleared a valid list — the new two_factor_email_fallback_enabled filter is consulted, defaulting to true so existing behaviour is unchanged for anyone not using the filter.

The new filter is also documented in readme.txt.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude (claude.ai)
Model(s): Claude Sonnet 4.6
Used for: Identifying the root cause, drafting the implementation.

Testing Instructions

  1. Install the plugin and configure a user with Two_Factor_Email enabled.
  2. Add the following to your theme's functions.php:
add_filter( 'two_factor_enabled_providers_for_user', function( $providers, $user_id ) {
    return []; // simulate trusted IP bypass
}, 10, 2 );
  1. Before this fix: log in as that user — you are still prompted for an email 2FA code (broken behaviour).
  2. Apply this fix, then additionally add:
add_filter( 'two_factor_email_fallback_enabled', function( $fallback, $user_id ) {
    return false;
}, 10, 2 );
  1. After this fix: log in as that user — 2FA is skipped entirely as intended ✅
  2. Remove both filters and verify the email fallback still triggers normally for a user whose provider class no longer exists (fail-safe preserved) ✅

Screenshots or screencast

N/A — no UI changes.

Changelog Entry

Fixed - Respect intentional empty provider list returned by two_factor_enabled_providers_for_user filter by adding the two_factor_email_fallback_enabled filter to allow opting out of the email fallback.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new hook to let integrators intentionally bypass the “email fallback” behavior when two_factor_enabled_providers_for_user clears a user’s enabled provider list, while preserving the existing fail-safe when providers genuinely disappear.

Changes:

  • Updates Two_Factor_Core::get_available_providers_for_user() to distinguish “providers missing” vs “providers intentionally cleared” and introduces the two_factor_email_fallback_enabled filter.
  • Documents the new filter in readme.txt.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
class-two-factor-core.php Adds logic + a new filter to optionally disable the email fallback when enabled providers are intentionally cleared.
readme.txt Documents the new two_factor_email_fallback_enabled filter and its purpose.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread class-two-factor-core.php
Comment on lines +702 to +725
$unfiltered = array_intersect( (array) $user_providers_raw, array_keys( $providers ) );

/**
* Filters whether the email provider fallback is applied when a user's
* enabled provider list resolves to empty but they have providers configured
* in user meta. Return false to disable the fallback and allow an empty
* provider list to pass through — for example, to bypass two-factor for
* trusted IP addresses.
*
* This filter only runs when the configured providers still exist. If
* providers are genuinely missing or removed, the fail-safe always applies
* regardless of this filter.
*
* @since 0.17.0
*
* @param bool $apply_fallback Whether to apply the email fallback. Default true.
* @param int $user_id The user ID.
*/
$apply_fallback = empty( $unfiltered ) || apply_filters( 'two_factor_email_fallback_enabled', true, $user->ID );

if ( $apply_fallback ) {
if ( isset( $providers['Two_Factor_Email'] ) ) {
// Force Emailed codes to 'on'.
$enabled_providers[] = 'Two_Factor_Email';
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new two_factor_email_fallback_enabled behavior changes the outcome of get_available_providers_for_user() when a valid provider list is intentionally cleared via two_factor_enabled_providers_for_user, but there’s no unit test coverage validating (1) fallback remains enabled by default and (2) the fallback can be disabled via the new filter while still preserving the existing fail-safe when providers are genuinely missing. Please add/extend tests (likely in tests/class-two-factor-core.php, near test_deprecated_provider_for_user) to cover these cases so regressions are caught.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nimesh-xecurify would you be able to support on unit test extension here?

@masteradhoc masteradhoc marked this pull request as ready for review April 19, 2026 17:53
@github-actions
Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @abovowebdevelopment, @sirolf.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Unlinked contributors: abovowebdevelopment, sirolf.

Co-authored-by: masteradhoc <masteradhoc@git.wordpress.org>
Co-authored-by: alexclst <alexclst@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@masteradhoc masteradhoc added this to the 0.17.0 milestone Apr 19, 2026
@masteradhoc masteradhoc self-assigned this Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The 'two_factor_enabled_providers_for_user' filter behaves differently in version 0.16.0

2 participants