Implementare il 2FA (autenticazione a due fattori) in WordPress

L’autenticazione a due fattori (2FA) aggiunge un secondo controllo oltre a utente e password, riducendo drasticamente i rischi di furto di account. In questa guida trovi due strade: plugin pronti per andare online in pochi minuti e un esempio minimale per inviare OTP via email senza dipendenze esterne.

Che cos’è il 2FA (e quali metodi usare)

  • TOTP (app di autenticazione): codici a tempo in Google Authenticator, 1Password, Authy, Microsoft Authenticator. Sicuro e offline.
  • WebAuthn/FIDO2: chiavi di sicurezza (YubiKey, passkey). È lo stato dell’arte.
  • OTP via email: comodo e senza app, buona soluzione “minimale”.
  • SMS: pratico ma meno sicuro; evita se possibile.

Opzione 1 — Attivare il 2FA con un plugin

  1. Vai in Plugin → Aggiungi nuovo e cerca “two factor” o “2FA”. Scegli un plugin affidabile con aggiornamenti recenti e molte installazioni attive.
  2. Installa e attiva. Poi apri le Impostazioni del plugin per:
    • Abilitare TOTP, Email OTP e (se supportato) WebAuthn/chiavi di sicurezza.
    • Obbligare il 2FA per determinati ruoli (es. Amministratori, Editor).
    • Generare codici di backup da conservare in un password manager.
    • Configurare il grace period (giorni per attivare il 2FA prima di renderlo obbligatorio).
  3. Comunica agli utenti come attivarlo nel proprio profilo (Il mio profilo → Sicurezza), fornendo istruzioni e link alla knowledge base.

Opzione 2 — Esempio minimale: OTP via email senza dipendenze

Di seguito trovi un’implementazione didattica basata su OTP via email. Funziona così:

  1. L’utente inserisce utente/password nella pagina di login.
  2. Se le credenziali sono corrette, WordPress non conclude il login: genera un codice OTP, lo invia via email e chiede di confermarlo.
  3. L’utente inserisce l’OTP in una pagina dedicata (/2fa); se è valido e non scaduto, si completa l’accesso.

Pro: zero dipendenze, immediato. Contro: non è forte quanto TOTP/WebAuthn. Usalo come base o fallback.

1) Aggiungi il codice al tema child o a un plugin mu-plugin

Incolla questo blocco in wp-content/mu-plugins/wp-2fa-email.php (consigliato) oppure nel functions.php del tema child:

<?php
/**
 * 2FA minimale via Email (OTP a 6 cifre).
 * Salva come: wp-content/mu-plugins/wp-2fa-email.php
 */

if (!defined('ABSPATH')) { exit; }

class WP_Minimal_Email_2FA {
    const TRANSIENT_PREFIX = 'wp2fa_pending_';
    const TRANSIENT_TTL    = 10 * MINUTE_IN_SECONDS; // OTP valido 10 minuti
    const OTP_LENGTH       = 6;

    public function __construct() {
        add_filter('authenticate', [$this, 'intercept_login'], 30, 3);
        add_shortcode('wp_2fa', [$this, 'render_2fa_form']);
        add_action('init', [$this, 'maybe_handle_2fa_submit']);
    }

    /**
     * Dopo username/password corrette, invia OTP e blocca il login
     */
    public function intercept_login($user, $username, $password) {
        if ($user instanceof WP_Error) {
            return $user; // credenziali errate o altri errori
        }
        if (!$user instanceof WP_User) {
            return $user; // niente da fare
        }

        // Genera token di sessione "pending" e OTP
        $token = wp_generate_password(20, false, false);
        $otp   = $this->generate_otp();

        $payload = [
            'user_id'   => $user->ID,
            'otp_hash'  => wp_hash_password($otp),
            'created'   => time(),
            'ip'        => $_SERVER['REMOTE_ADDR'] ?? '',
            'user_agent'=> $_SERVER['HTTP_USER_AGENT'] ?? '',
        ];

        set_transient(self::TRANSIENT_PREFIX . $token, $payload, self::TRANSIENT_TTL);

        // Invia OTP via email
        $to      = $user->user_email;
        $subject = sprintf(__('Il tuo codice di verifica (%s)'), get_bloginfo('name'));
        $message = sprintf(
            __("Ciao %s,\n\necco il tuo codice di verifica per accedere a %s:\n\n%s\n\nScade tra 10 minuti.\nSe non hai richiesto il codice, ignora questa email."),
            $user->display_name ?: $user->user_login,
            home_url(),
            $otp
        );
        wp_mail($to, $subject, $message);

        // Messaggio con link alla pagina 2FA
        $twofa_url = add_query_arg(['token' => $token], home_url('/2fa/'));
        $error_msg = sprintf(
            wp_kses_post(__('Abbiamo inviato un codice di verifica al tuo indirizzo email. <a href="%s">Inseriscilo qui per completare l\'accesso</a>.', 'default')),
            esc_url($twofa_url)
        );

        return new WP_Error('two_factor_required', $error_msg);
    }

    /**
     * Shortcode [wp_2fa] per mostrare il form OTP
     */
    public function render_2fa_form($atts) {
        $token = isset($_GET['token']) ? sanitize_text_field($_GET['token']) : '';
        $nonce = wp_create_nonce('wp2fa_verify');

        ob_start();
        ?>
        <h2>Verifica in due passaggi</h2>
        <p>Inserisci il codice di 6 cifre ricevuto via email.</p>

        <form method="post">
            <label for="otp">Codice OTP</label><br>
            <input type="text" id="otp" name="otp" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required><br><br>

            <input type="hidden" name="token" value="<?= esc_attr($token); ?>">
            <input type="hidden" name="wp2fa_action" value="verify">
            <input type="hidden" name="wp2fa_nonce" value="<?= esc_attr($nonce); ?>">

            <button type="submit">Verifica e accedi</button>
        </form>
        <?php
        return ob_get_clean();
    }

    /**
     * Gestisce l'invio del form OTP e completa il login
     */
    public function maybe_handle_2fa_submit() {
        if (!isset($_POST['wp2fa_action']) || $_POST['wp2fa_action'] !== 'verify') {
            return;
        }
        if (!isset($_POST['wp2fa_nonce']) || !wp_verify_nonce($_POST['wp2fa_nonce'], 'wp2fa_verify')) {
            wp_die(__('Token di sicurezza non valido.'));
        }

        $token = isset($_POST['token']) ? sanitize_text_field($_POST['token']) : '';
        $otp   = isset($_POST['otp']) ? preg_replace('/\D+/', '', $_POST['otp']) : '';

        if (strlen($otp) !== self::OTP_LENGTH || empty($token)) {
            wp_die(__('Codice o token non valido.'));
        }

        $payload = get_transient(self::TRANSIENT_PREFIX . $token);
        if (!$payload) {
            wp_die(__('Codice scaduto o non trovato. Riprova ad accedere.'));
        }

        if (!wp_check_password($otp, $payload['otp_hash'])) {
            wp_die(__('Codice errato. Riprova.'));
        }

        $user_id = intval($payload['user_id']);
        $user    = get_user_by('id', $user_id);
        if (!$user) {
            wp_die(__('Utente non trovato.'));
        }

        // Completa l'accesso
        wp_set_current_user($user_id);
        wp_set_auth_cookie($user_id, true); // true = "ricordami"
        delete_transient(self::TRANSIENT_PREFIX . $token);

        // Reindirizza alla bacheca
        wp_safe_redirect(admin_url());
        exit;
    }

    private function generate_otp() {
        $min = pow(10, self::OTP_LENGTH - 1); // 100000
        $max = pow(10, self::OTP_LENGTH) - 1; // 999999
        return (string) wp_rand($min, $max);
    }
}

new WP_Minimal_Email_2FA();

2) Crea la pagina per inserire l’OTP

  1. Vai in Pagine → Aggiungi nuova e crea “2FA”.
  2. Nel contenuto inserisci lo shortcode:
    [wp_2fa]
  3. Pubblica la pagina e verifica che sia raggiungibile in /2fa/ (o l’URL scelto).

3) Test rapido

  1. Esci dall’admin e prova a fare login con un account valido.
  2. Dovresti vedere un messaggio che ti invita a inserire il codice. Apri l’email, copia l’OTP, vai su /2fa/ e conferma.
  3. Al successo verrai reindirizzato alla Bacheca.

Hardening e miglioramenti consigliati

  • Rate limiting: aggiungi un contatore tentativi OTP (es. con user_meta) e blocca per alcuni minuti dopo N errori.
  • Scadenze brevi: 5–10 minuti sono un buon compromesso per l’OTP.
  • IP binding: opzionale, ma puoi controllare l’IP presente nel payload per ridurre i rischi di hijacking di token.
  • HTTPS obbligatorio: assicurati che il sito usi sempre TLS.
  • Backup codes: fornisci codici di recupero monouso per gli amministratori.
  • Audit: registra login riusciti/respinti con data, IP e user agent.

Alternative più robuste (TOTP/WebAuthn)

Per produzione, preferisci un plugin che offra TOTP e WebAuthn. Se vuoi estendere il codice personalizzato:

  • TOTP: genera e salva un secret per utente, mostra un QR con schema otpauth://, verifica i codici con una libreria TOTP.
  • WebAuthn: integra registrazione e autenticazione di chiavi FIDO2. Richiede una libreria dedicata lato server.

Bozza di generazione URL TOTP (concettuale)

<?php
// ESEMPIO CONCETTUALE (usa una libreria TOTP in produzione)
$user    = wp_get_current_user();
$issuer  = rawurlencode(get_bloginfo('name'));
$account = rawurlencode($user->user_email);
$secret  = 'JBSWY3DPEHPK3PXP'; // genera e salva un secret Base32 per l'utente

$otpauth = sprintf('otpauth://totp/%s:%s?secret=%s&issuer=%s&digits=6&period=30&algorithm=SHA1',
$issuer,
$account,
$secret,
$issuer
);

// Mostra QR con una libreria/servizio QR e salva $secret in user_meta
update_user_meta($user->ID, 'totp_secret', $secret); 

Checklist di rollout

  • Abilita 2FA prima sugli account admin, poi sugli editor, infine sugli autori.
  • Prepara una guida per gli utenti con screenshot e domande frequenti.
  • Configura codici di backup e una procedura di sblocco (su ticket, solo dopo verifica identità).
  • Monitora gli errori di login per la prima settimana e misura l’adozione.

WP-CLI e automatizzazione

Alcuni plugin espongono comandi WP-CLI per forzare il 2FA su ruoli/utenti. Se disponibile, puoi abilitare in blocco:

# Esempio generico (varia in base al plugin scelto)
wp twofactor enforce --role=administrator --require=totp,email --grace=7

Domande frequenti

E se un utente perde l’accesso al secondo fattore? Usa i codici di backup o una procedura di recupero gestita dall’amministratore. Evita di disabilitare il 2FA permanentemente.

Il 2FA rallenta il login? Un po’, ma per gli amministratori il beneficio in sicurezza è enorme; per gli autori valuta Email OTP o “remember this device”.

Conclusione

Per un sito produttivo la scelta migliore è un plugin 2FA maturo con TOTP e WebAuthn, enforcement per ruolo e codici di backup. L’esempio OTP via email qui sopra è utile come base, fallback o per ambienti interni: semplice, trasparente e senza dipendenze, ma meno robusto dei metodi moderni.

Torna su