Cómo añadir autenticación de dos factores (2FA) a WordPress sin plugins
¡Hola! Si estás leyendo esto, probablemente te preocupa la seguridad de tu sitio web, y haces muy bien. WordPress es el motor de más del 40% de la web, lo que lo convierte en un objetivo gigantesco para los ciberdelincuentes. La primera línea de defensa es, sin duda, una contraseña robusta, pero hoy vamos a dar un paso más allá.
Vamos a hablar de cómo añadir autenticación de dos factores (2FA) a tu WordPress, pero con un truco especial: sin necesidad de instalar plugins adicionales. Esto es perfecto si quieres mantener tu sitio ligero, evitar conflictos con otros plugins o simplemente te apetece entender cómo funciona la magia por dentro.
A lo largo de esta guía extensa, te explicaré paso a paso, con un lenguaje claro y sin tecnicismos innecesarios, cómo implementar el 2FA wordpress de forma manual. Aprenderás a generar códigos únicos con tu móvil y a configurar tu sitio para que los exija en cada inicio de sesión.
¿Preparado? Vamos a blindar tu WordPress.
¿Por qué necesitas la autenticación de dos factores?
Antes de ensuciarnos las manos con código, es crucial entender el "por qué". Una contraseña, por muy fuerte que sea, puede ser robada mediante ataques de phishing, keyloggers o simplemente por reutilizarla en otros sitios que han sido comprometidos.
La autenticacion dos factores añade una segunda capa de seguridad. Funciona así: "algo que sabes" (tu contraseña) + "algo que tienes" (tu teléfono móvil). Incluso si un hacker consigue tu contraseña, sin tu teléfono, no podrá entrar. Es como tener una caja fuerte con dos llaves diferentes: necesitas ambas para abrirla.
Añadir two factor authentication wordpress reduce drásticamente el riesgo de accesos no autorizados, protegiendo tus datos, los de tus usuarios y la reputación de tu sitio.
El plan: Cómo funciona el 2FA sin plugins
Vamos a utilizar el estándar de la industria llamado TOTP (Time-based One-Time Password). Es el mismo sistema que usan Google Authenticator, Authy o Microsoft Authenticator. La idea es que tu servidor y tu aplicación de teléfono compartan un "secreto". Cada 30 segundos, ambos generan un código de 6 dígitos basado en ese secreto y en la hora actual. Si coinciden, ¡eres tú!
Para ello, necesitaremos hacer dos cosas:
- Generar un secreto y mostrarlo en un código QR para que puedas escanearlo con tu app de autenticación.
- Verificar el código en el momento del login, justo después de introducir la contraseña.
Haremos esto añadiendo un pequeño script personalizado a tu tema. No te preocupes, es más fácil de lo que parece.
Requisitos previos
Antes de empezar, asegúrate de tener lo siguiente:
- Acceso al administrador de WordPress (para editar archivos del tema).
- Un tema hijo (Child Theme) activo. Esto es CRÍTICO. Si editas el archivo
functions.phpdel tema principal, perderás todos los cambios cuando actualices el tema. Si no sabes qué es un tema hijo, es un tema que hereda las funcionalidades del principal pero que puedes modificar sin romper nada. Crea uno o instala uno antes de continuar. - Una aplicación de autenticación en tu móvil (Google Authenticator, Authy, 1Password, etc.).
- Un editor de código o el administrador de archivos de tu hosting.
[WARNING] Este proceso requiere editar código. Aunque lo explicaremos paso a paso, un error podría romper tu sitio. Si no te sientes seguro, te recomendamos hacer una copia de seguridad completa antes de empezar.
Paso 1: Configurar el panel de administración
Primero, necesitamos un lugar en el panel de administración de WordPress para mostrar el código QR y el secreto. Lo haremos de forma sencilla añadiendo una sección en tu perfil de usuario. Así, cada usuario podrá configurar su propio 2FA.
1.1 Añadir el campo secreto al perfil
Vamos a añadir un campo donde guardaremos el secreto de cada usuario. Para ello, editaremos el archivo functions.php de tu tema hijo.
Abre el archivo y añade el siguiente código al final:
// Añadir campo de secreto 2FA al perfil de usuario
function mi_2fa_mostrar_campo_perfil( $user ) {
$secreto = get_user_meta( $user->ID, 'mi_2fa_secreto', true );
if ( empty( $secreto ) ) {
$secreto = '';
}
?>
<h3>Autenticación de Dos Factores</h3>
<table class="form-table">
<tr>
<th><label for="mi_2fa_secreto">Secreto 2FA</label></th>
<td>
<input type="text" name="mi_2fa_secreto" id="mi_2fa_secreto" value="<?php echo esc_attr( $secreto ); ?>" class="regular-text" />
<p class="description">Este es el secreto compartido. Guárdalo o escanéalo con tu aplicación.</p>
</td>
</tr>
</table>
<?php
}
add_action( 'show_user_profile', 'mi_2fa_mostrar_campo_perfil' );
add_action( 'edit_user_profile', 'mi_2fa_mostrar_campo_perfil' );
// Guardar el secreto cuando se actualiza el perfil
function mi_2fa_guardar_secreto_perfil( $user_id ) {
if ( !current_user_can( 'edit_user', $user_id ) ) {
return false;
}
if ( isset( $_POST['mi_2fa_secreto'] ) ) {
update_user_meta( $user_id, 'mi_2fa_secreto', sanitize_text_field( $_POST['mi_2fa_secreto'] ) );
}
}
add_action( 'personal_options_update', 'mi_2fa_guardar_secreto_perfil' );
add_action( 'edit_user_profile_update', 'mi_2fa_guardar_secreto_perfil' );
¿Qué hemos hecho?
- Hemos creado una función que muestra un nuevo campo llamado "Secreto 2FA" en la pantalla de edición de perfil.
- Hemos creado otra función que guarda ese valor en la base de datos cuando el usuario actualiza su perfil.
Ahora, ve a tu perfil en WordPress (Usuarios > Tu Perfil). Deberías ver la nueva sección "Autenticación de Dos Factores" con un campo de texto vacío. No te preocupes, aún no tiene un valor. Lo generaremos en el siguiente paso.
Paso 2: Generar el secreto y el código QR
Ahora viene la parte divertida. Necesitamos una función que genere un secreto aleatorio y que nos permita mostrarlo como un código QR. Para generar el QR, usaremos una librería externa llamada phpqrcode. Es muy ligera y fácil de integrar.
2.1 Descargar la librería PHP QR Code
Ve a la web oficial de phpqrcode (o busca en GitHub "phpqrcode"). Descarga el archivo qrlib.php y súbelo a una carpeta en tu tema hijo, por ejemplo, /mi-tema-hijo/inc/qrlib.php.
2.2 Crear la página de configuración
Vamos a crear una página específica en el admin para generar el secreto. Añade este código al functions.php:
// Crear un menú de configuración para el 2FA
function mi_2fa_crear_menu() {
add_menu_page( 'Configurar 2FA', 'Configurar 2FA', 'manage_options', 'mi-2fa-config', 'mi_2fa_pagina_config', 'dashicons-shield', 99 );
}
add_action( 'admin_menu', 'mi_2fa_crear_menu' );
// Función para mostrar la página de configuración
function mi_2fa_pagina_config() {
// Generar un secreto si no existe
$secreto = get_user_meta( get_current_user_id(), 'mi_2fa_secreto', true );
if ( empty( $secreto ) ) {
$secreto = mi_2fa_generar_secreto();
update_user_meta( get_current_user_id(), 'mi_2fa_secreto', $secreto );
}
// Crear el código QR
$nombre_sitio = get_bloginfo( 'name' );
$usuario = wp_get_current_user();
$emisor = 'WordPress';
$datos_qr = "otpauth://totp/{$emisor}:{$usuario->user_login}?secret={$secreto}&issuer={$emisor}";
// Incluir la librería QR
include( get_stylesheet_directory() . '/inc/qrlib.php' );
QRcode::png( $datos_qr, false, QR_ECLEVEL_L, 10 );
?>
<div class="wrap">
<h1>Configurar Autenticación de Dos Factores</h1>
<p>Escanea el siguiente código QR con tu aplicación de autenticación (Google Authenticator, Authy, etc.).</p>
<p>Si no puedes escanear, copia el secreto manualmente: <strong><?php echo esc_html( $secreto ); ?></strong></p>
<p>Después de escanear, guarda los cambios en tu perfil. El secreto ya está guardado para tu usuario.</p>
</div>
<?php
}
// Función para generar un secreto aleatorio (Base32)
function mi_2fa_generar_secreto( $longitud = 16 ) {
$caracteres = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // Base32
$secreto = '';
for ( $i = 0; $i < $longitud; $i++ ) {
$secreto .= $caracteres[ random_int( 0, strlen( $caracteres ) - 1 ) ];
}
return $secreto;
}
¿Qué hemos hecho?
- Hemos creado un nuevo menú en el panel de administración llamado "Configurar 2FA".
- Al visitar esa página, la función genera un secreto aleatorio (si no existe) y lo guarda automáticamente para el usuario actual.
- Luego, genera un código QR con los datos necesarios (nombre del sitio, usuario y secreto) y lo muestra directamente en pantalla.
[INFO] El secreto se genera una sola vez. Si ya existe uno, mostrará la misma página con el mismo QR. Si quieres regenerarlo, deberías borrar el valor del campo en tu perfil primero.
Paso 3: Verificar el código en el login
Este es el paso más importante. Aquí es donde interceptamos el proceso de inicio de sesión y exigimos el código de 6 dígitos.
3.1 Añadir el campo de código al formulario de login
Añade este código al functions.php para mostrar un campo extra en el formulario de login:
// Añadir campo de código 2FA al formulario de login
function mi_2fa_agregar_campo_login() {
?>
<p>
<label for="mi_2fa_codigo">Código 2FA<br>
<input type="text" name="mi_2fa_codigo" id="mi_2fa_codigo" class="input" size="20" autocomplete="off" />
</label>
</p>
<?php
}
add_action( 'login_form', 'mi_2fa_agregar_campo_login' );
3.2 Verificar el código antes de permitir el acceso
Ahora, modificaremos la autenticación. Añadimos una función que se ejecuta justo después de que WordPress verifica la contraseña.
// Verificar el código 2FA
function mi_2fa_verificar_login( $user, $username, $password ) {
// Si el usuario ya está autenticado, no hacemos nada.
if ( !is_a( $user, 'WP_User' ) ) {
return $user;
}
// Obtener el código enviado por el usuario
$codigo_enviado = isset( $_POST['mi_2fa_codigo'] ) ? sanitize_text_field( $_POST['mi_2fa_codigo'] ) : '';
// Obtener el secreto del usuario
$secreto = get_user_meta( $user->ID, 'mi_2fa_secreto', true );
// Si el usuario no tiene secreto, no exigimos código.
if ( empty( $secreto ) ) {
return $user;
}
// Verificar el código TOTP
if ( !mi_2fa_validar_codigo_totp( $secreto, $codigo_enviado ) ) {
return new WP_Error( 'autenticacion_fallida', __( 'Código 2FA incorrecto. Por favor, inténtalo de nuevo.' ) );
}
return $user;
}
add_filter( 'authenticate', 'mi_2fa_verificar_login', 10, 3 );
3.3 La función de validación TOTP
Aquí está el corazón del sistema. Esta función calcula el código TOTP esperado y lo compara con el que el usuario ha introducido.
// Función para validar el código TOTP
function mi_2fa_validar_codigo_totp( $secreto, $codigo ) {
// Si el código está vacío, falla.
if ( empty( $codigo ) ) {
return false;
}
// Decodificar el secreto Base32
$clave = mi_2fa_base32_decodificar( $secreto );
// Obtener el timestamp actual en intervalos de 30 segundos
$contador = floor( time() / 30 );
// Comprobar 3 intervalos (el actual, el anterior y el siguiente) para evitar problemas de sincronización
for ( $i = -1; $i <= 1; $i++ ) {
$hash = hash_hmac( 'sha1', pack( 'N*', 0, $contador + $i ), $clave, true );
$offset = ord( substr( $hash, -1 ) ) & 0x0F;
$valor = ( ( ( ord( substr( $hash, $offset, 1 ) ) & 0x7F ) << 24 ) |
( ( ord( substr( $hash, $offset + 1, 1 ) ) & 0xFF ) << 16 ) |
( ( ord( substr( $hash, $offset + 2, 1 ) ) & 0xFF ) << 8 ) |
( ord( substr( $hash, $offset + 3, 1 ) ) & 0xFF ) );
$codigo_esperado = $valor % 1000000; // 6 dígitos
$codigo_esperado = str_pad( $codigo_esperado, 6, '0', STR_PAD_LEFT );
if ( hash_equals( $codigo_esperado, $codigo ) ) {
return true;
}
}
return false;
}
// Función para decodificar Base32
function mi_2fa_base32_decodificar( $datos ) {
$base32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$datos = strtoupper( $datos );
$bits = '';
$longitud = strlen( $datos );
for ( $i = 0; $i < $longitud; $i++ ) {
$pos = strpos( $base32, $datos[$i] );
if ( $pos === false ) {
continue; // Ignorar caracteres inválidos
}
$bits .= str_pad( decbin( $pos ), 5, '0', STR_PAD_LEFT );
}
$bytes = '';
$longitud_bits = strlen( $bits );
for ( $i = 0; $i + 8 <= $longitud_bits; $i += 8 ) {
$bytes .= chr( bindec( substr( $bits, $i, 8 ) ) );
}
return $bytes;
}
¿Qué hemos hecho?
- Hemos añadido un filtro a la autenticación de WordPress.
- Si el usuario tiene un secreto guardado, exigimos el código.
- La función
mi_2fa_validar_codigo_totpcalcula el código esperado usando el algoritmo estándar y lo compara con el introducido. - Permitimos un margen de ±1 intervalo (30 segundos) para compensar la deriva del reloj del tel
