Summary
The TOTP setup on the profile page uses a two-step save flow:
- "Submit" button (inline, next to the code input) — fires a REST API call (
POST /two-factor/1.0/totp) that validates the authenticator code and saves _two_factor_totp_key.
- "Update Profile" button (bottom of page) — submits the main profile form, which saves
_two_factor_enabled_providers with whatever checkboxes are checked.
If a user checks the TOTP "Enabled" checkbox, skips the "Submit" verification button, and clicks "Update Profile" directly, _two_factor_enabled_providers is saved with Two_Factor_Totp listed but _two_factor_totp_key was never written. This creates an inconsistent state.
Steps to Reproduce
- Go to your profile page with Two Factor active.
- Check the "Enabled" checkbox next to Time Based One-Time Password (TOTP).
- Do not scan the QR code or click the "Submit" verification button.
- Scroll down and click "Update Profile."
- Check user meta:
_two_factor_enabled_providers now includes Two_Factor_Totp, but _two_factor_totp_key is empty.
Expected Behavior
The server-side profile save handler (personal_options_update / edit_user_profile_update) should validate that the TOTP key exists in user meta before allowing Two_Factor_Totp to be included in _two_factor_enabled_providers. If the key is missing, the provider should be silently removed from the enabled list and/or an admin notice should warn the user that TOTP setup is incomplete.
The PR #643 improved the JS side (auto-checking the Enabled box after successful REST verification), but the server side still allows saving an invalid provider configuration.
Impact
The inconsistent state causes Two_Factor_Totp::is_available_for_user() to return false (no key), which causes get_primary_provider_for_user() to silently fall back to another provider (typically Backup Codes). See #796 for the downstream impact of the silent fallback.
This is easy to trigger accidentally — the "Update Profile" button is the large, prominent button at the bottom of the page, while the TOTP "Submit" verification button is a small inline button that users naturally overlook.
Suggested Fix
In the personal_options_update handler where _two_factor_enabled_providers is saved, add a check:
if ( in_array( 'Two_Factor_Totp', $providers, true ) ) {
$key = get_user_meta( $user_id, self::SECRET_META_KEY, true );
if ( empty( $key ) ) {
// Remove TOTP from enabled providers — setup was never completed.
$providers = array_diff( $providers, array( 'Two_Factor_Totp' ) );
// Optionally: set an admin notice warning the user.
}
}
Environment
- Two Factor 0.14.2
- WordPress 6.7.x
- Tested on both MySQL and SQLite backends
Related Issues
Summary
The TOTP setup on the profile page uses a two-step save flow:
POST /two-factor/1.0/totp) that validates the authenticator code and saves_two_factor_totp_key._two_factor_enabled_providerswith whatever checkboxes are checked.If a user checks the TOTP "Enabled" checkbox, skips the "Submit" verification button, and clicks "Update Profile" directly,
_two_factor_enabled_providersis saved withTwo_Factor_Totplisted but_two_factor_totp_keywas never written. This creates an inconsistent state.Steps to Reproduce
_two_factor_enabled_providersnow includesTwo_Factor_Totp, but_two_factor_totp_keyis empty.Expected Behavior
The server-side profile save handler (
personal_options_update/edit_user_profile_update) should validate that the TOTP key exists in user meta before allowingTwo_Factor_Totpto be included in_two_factor_enabled_providers. If the key is missing, the provider should be silently removed from the enabled list and/or an admin notice should warn the user that TOTP setup is incomplete.The PR #643 improved the JS side (auto-checking the Enabled box after successful REST verification), but the server side still allows saving an invalid provider configuration.
Impact
The inconsistent state causes
Two_Factor_Totp::is_available_for_user()to returnfalse(no key), which causesget_primary_provider_for_user()to silently fall back to another provider (typically Backup Codes). See #796 for the downstream impact of the silent fallback.This is easy to trigger accidentally — the "Update Profile" button is the large, prominent button at the bottom of the page, while the TOTP "Submit" verification button is a small inline button that users naturally overlook.
Suggested Fix
In the
personal_options_updatehandler where_two_factor_enabled_providersis saved, add a check:Environment
Related Issues