Skip to content

add wp-config support to disable 2fa for a user#908

Open
masteradhoc wants to merge 5 commits into
WordPress:masterfrom
masteradhoc:621-support-wpconfig-entry
Open

add wp-config support to disable 2fa for a user#908
masteradhoc wants to merge 5 commits into
WordPress:masterfrom
masteradhoc:621-support-wpconfig-entry

Conversation

@masteradhoc

@masteradhoc masteradhoc commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

What?

Adds a TWO_FACTOR_DISABLE_FOR_USER wp-config constant that disables the two-factor login challenge for one or more named users, so an operator with filesystem access can recover a locked-out account without database surgery or a second admin.

Fixes # TBD

Why?

When a user loses their second factor and the site has no other administrator, today's only operator options are editing the database directly or un-checking options as a second admin. There is no supported wp-config.php lever to bypass 2FA for a single account.

This implements a per-user wp-config constant to improve the account recovery possibilities. Crucially, it is per-user, not site-wide, because disabling 2FA for everyone just because one person is locked out throws away the protection of every other account. Because the constant lives in wp-config.php (filesystem access only), it carries the same trust level as shell/DB access and adds no new attack surface.

Related discussion:

How?

Two small functions in two-factor.php, registered in two_factor_register_admin_hooks():

  • two_factor_bypass_primary_provider_for_user() hooks the existing two_factor_primary_provider_for_user filter. For a matching user it returns null, which makes Two_Factor_Core::is_user_using_two_factor() report false so wp_login() lets the user through without a second factor.
    • This hook point is deliberate: it runs after core's fail-closed logic, so it does not trip the fallback that would otherwise force-enable emailed codes (which is what would happen if the enabled/supported providers were emptied instead).
    • The matched user is loaded once with get_userdata() and compared by ID, login, or email. The constant accepts a single identifier, a comma-separated list, or an array.
  • two_factor_bypass_admin_notice() shows a standing admin_notices warning (to manage_options users) whenever the constant is set, echoing the configured value so a forgotten bypass — or a typo — is easy to spot.

Usage in wp-config.php:

define( 'TWO_FACTOR_DISABLE_FOR_USER', 'admin' );                       // one user
define( 'TWO_FACTOR_DISABLE_FOR_USER', 'admin, secondadmin' );            // CSV list
define( 'TWO_FACTOR_DISABLE_FOR_USER', array( 'admin', 'secondadmin' ) ); // array

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Drafting the implementation and PR text. The hook choice, scope decision (per-user, no global switch), and final code were reviewed and edited by me.

Testing Instructions

  1. Pick a user who has 2FA enabled (e.g. admin) and confirm they normally get the second-factor prompt at login.
  2. Add define( 'TWO_FACTOR_DISABLE_FOR_USER', 'admin' ); to wp-config.php, then log out and log in as admin with password only — confirm no 2FA prompt.
  3. Log in as a different 2FA user not named in the constant — confirm they still get the 2FA prompt.
  4. Repeat step 2 using each identifier form (user ID, email, and a comma-separated list of two users) — confirm the bypass applies in each case.
  5. With the constant set, open any wp-admin page as an administrator — confirm the yellow warning notice appears showing the configured value.
  6. Remove the constant, log out and back in as admin — confirm the 2FA prompt returns and the notice is gone.
  7. Negative: set the constant to a non-existent identifier (e.g. 'nobody') — confirm no user is bypassed and real 2FA users are still challenged.

Screenshots or screencast

image

Changelog Entry

Added - TWO_FACTOR_DISABLE_FOR_USER wp-config constant to bypass two-factor for one or more named users (by ID, login, or email) for account recovery, with an admin notice while a bypass is active.

Open WordPress Playground Preview

@masteradhoc masteradhoc added this to the 0.17.0 milestone Jun 14, 2026
@masteradhoc masteradhoc self-assigned this Jun 14, 2026
@github-actions

github-actions Bot commented Jun 14, 2026

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.

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

Co-authored-by: masteradhoc <masteradhoc@git.wordpress.org>
Co-authored-by: nimesh-xecurify <nimeshatxecurify@git.wordpress.org>
Co-authored-by: dknauss <dpknauss@git.wordpress.org>

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

masteradhoc and others added 4 commits July 12, 2026 20:47
Verifies the functionality of the 2FA bypass mechanism, including user matching by ID, login, and email, support for comma-separated strings, and the display of the administrative warning notice.
This prevents potential errors if the `two_factor_primary_provider_for_user` filter returns an empty value.
add tests for the TWO_FACTOR_DISABLE_FOR_USER constant
@dknauss

dknauss commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Small Playground preview fix: the current preview URL points at https://github.com/WordPress/two-factor.git with ref 621-support-wpconfig-entry, but that branch only exists on the contributor fork, not upstream. I verified:

  • WordPress/two-factor:621-support-wpconfig-entry -> 404
  • masteradhoc/two-factor:621-support-wpconfig-entry -> 9390b115cb755c6a8787cdf2317828961862ee49

So the minimal fix is just changing the git:directory resource URL to:

"url": "https://github.com/masteradhoc/two-factor.git",
"ref": "621-support-wpconfig-entry"

If we want the Playground link to demo this feature rather than only install the PR branch, this blueprint also defines the wp-config constant, seeds the admin user with the email provider, logs in, and lands in wp-admin where the bypass notice should be visible:

{"$schema":"https://playground.wordpress.net/blueprint-schema.json","preferredVersions":{"php":"8.2","wp":"latest"},"landingPage":"/wp-admin/","steps":[{"step":"defineWpConfigConsts","consts":{"TWO_FACTOR_DISABLE_FOR_USER":"admin"}},{"step":"installPlugin","pluginData":{"resource":"git:directory","url":"https://github.com/masteradhoc/two-factor.git","ref":"621-support-wpconfig-entry","path":"/"},"options":{"activate":true}},{"step":"runPHP","code":"<?php require_once '/wordpress/wp-load.php'; $user = get_user_by( 'login', 'admin' ); if ( $user && class_exists( 'Two_Factor_Core' ) ) { 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' ); }"},{"step":"login","username":"admin","password":"password"}]}

Openable version: Playground demo for the wp-config per-user 2FA bypass

@dknauss

dknauss commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@masteradhoc instead of writing exceptions into wp-config.php, my preference would be a WP-CLI one-time/expiring per-user recovery command. For example:

  • wp 2fa disable user@example.com
  • wp 2fa reset user@example.com
  • wp 2fa bypass user@example.com --expires=15m

For c3d759c, if the intention is not to provide a sitewide killswitch, this intention is defeated by allowing unlimited users to be defined as exceptions. It also opens some new error and abuse potential:

  • Email exposure: the notice prints all configured emails/logins/IDs to anyone with manage_options. That is not public, but it is an unnecessary exposure.
  • Wrong login/email/ID in exception list: no error, silently no match. The admin notice echoes the configured values, but it does not validate it or confirm “0 users matched.”
  • Duplicate login/emails in exception list:
    1. Repetition of same user by id, username, or email in the exception list does not create an error. It just gives three chances to match the same user. Same for duplicates like admin, admin, admin: no error, just excess parsing/comparison.
    2. If the database actually contains multiple users with the same email or username, every matching user will be bypassed.
  • Lazy-admin risk: A CSV/array list makes this a convenient “temporarily disable 2FA for the leadership team” mechanism. The standing notice helps, but it does not prevent normalization of a bypass.
  • Big lists of exceptions will flood the admin notice. If someone puts 1000 exceptions in an array, the admin notice will print all 1000. If they put one giant CSV string in wp-config.php, it will print that whole string.

So, if wp-config.php is used, it should be tightly scoped: a limited, loggable, single-user constant (exactly one username) with loud admin/network notices, and browser-login-only scope:

  • Resolve the username to one user — who should be an Administrator.
  • Do not accept an email address or CSV/array.
  • Do not disable two-factor on other surfaces. (The current hook makes the user globally “not using 2FA.”)
  • If no real user matches, show a loud “configured username does not exist”
  • If the matching user is not an admin, flag that in the notification too.
  • If the username matches to more than one user (from database corruption or direct manipulation) flag that too.

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.

3 participants