Skip to content

TOTP setup: Update Profile saves enabled-providers without validating TOTP key exists #797

@dknauss

Description

@dknauss

Summary

The TOTP setup on the profile page uses a two-step save flow:

  1. "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.
  2. "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

  1. Go to your profile page with Two Factor active.
  2. Check the "Enabled" checkbox next to Time Based One-Time Password (TOTP).
  3. Do not scan the QR code or click the "Submit" verification button.
  4. Scroll down and click "Update Profile."
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    TOTPTime-based One-time Passwords

    Type

    Projects

    Status

    In progress

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions