Two-factor authentication (2FA) requires a second proof of identity beyond the password, making brute-forced or leaked credentials alone insufficient to compromise a WordPress account. Even with strong passwords and login-page protection via .htaccess rules, a single phished or reused password exposes the entire admin panel without 2FA. Time-based One-Time Passwords (TOTP) are the most widely adopted 2FA standard — the server and the authenticator app both derive a six-digit code from a shared secret and the current Unix timestamp, with codes changing every 30 seconds. Implementing TOTP in WordPress without a third-party plugin requires the OTPHP PHP library, a QR code generator, and two additional admin screens: one for enrolling the user and one for entering the code at login. The enrollment flow generates a base32 secret with TOTP::create(), stores it in user meta after the user scans the QR code and confirms with a valid token, and marks the user as 2FA-enrolled in a separate meta flag. The verification flow hooks into wp_authenticate to intercept the login after password verification and redirect to a custom OTP entry form before granting access. Sessions in transit between the password-check step and the OTP step must be carried by a short-lived, cryptographically random token stored in user meta rather than a full authentication cookie, which must not be issued until 2FA passes. Backup codes — a set of single-use recovery codes — allow users to regain access if they lose their authenticator device, and should be hashed before storage the same way passwords are. The XML-RPC and login protection post covers the server-level measures that complement 2FA — combined, they eliminate both automated brute-force and single-factor credential compromise. The nonce and sanitization post applies the same input-handling discipline to the OTP entry form that is essential for any security-critical POST endpoint. Requiring 2FA only for administrator and editor roles while leaving subscriber accounts unchallenged is a practical balance between security and user friction for most sites.
Problem: WordPress admin accounts protected only by a password remain vulnerable to credential stuffing and phishing attacks — a single leaked password grants full site access without a second authentication factor.
Solution: Implement TOTP two-factor authentication by generating a shared secret on enrollment, storing it hashed in user meta, and intercepting login via wp_authenticate to require a valid one-time code before issuing the auth cookie.
// Requires: composer require spomky-labs/otphp
// Place in mu-plugins/two-factor-auth.php
use OTPHP\TOTP;
// Step 1: Generate and store the TOTP secret for a user
function enroll_user_totp(int $user_id): string {
$totp = TOTP::create();
$totp->setLabel(get_userdata($user_id)->user_email);
$totp->setIssuer(get_bloginfo('name'));
$secret = $totp->getSecret();
update_user_meta($user_id, '_2fa_secret', $secret);
update_user_meta($user_id, '_2fa_enabled', '0'); // confirmed after first verify
return $totp->getProvisioningUri(); // used to generate QR code
}
// Step 2: Verify a submitted OTP and mark enrollment complete
function verify_totp(int $user_id, string $otp): bool {
$secret = get_user_meta($user_id, '_2fa_secret', true);
if (!$secret) return false;
$totp = TOTP::create($secret);
if ($totp->verify($otp, null, 1)) { // window = 1 (allow 30s clock skew)
update_user_meta($user_id, '_2fa_enabled', '1');
return true;
}
return false;
}
// Step 3: Intercept login and challenge 2FA-enrolled users
add_filter('wp_authenticate_user', function($user, $password) {
if (is_wp_error($user)) return $user;
if (get_user_meta($user->ID, '_2fa_enabled', true) !== '1') return $user;
// Store pending auth token; return error to block cookie issuance
$token = wp_generate_password(32, false);
set_transient('2fa_pending_' . $token, $user->ID, 300);
wp_redirect(site_url('/wp-login.php?action=2fa&token=' . $token));
exit;
}, 10, 2);
NOTE: This is a minimal skeleton — a production implementation must also handle the OTP entry form rendering, backup codes, account lockout after N failed attempts, and a role check so only administrator and editor accounts are challenged. Consider using the established Two Factor plugin (by WordPress.org) for production sites rather than maintaining a custom implementation.