Como añadir 2FA básico por email en el login con PHP en WordPress

Contents

Introducción

Este tutorial explica, con todo lujo de detalles, cómo añadir una verificación en dos pasos (2FA) básica mediante correo electrónico en el proceso de login de WordPress usando PHP. La solución propuesta es un plugin ligero que detiene el inicio de sesión después de validar usuario y contraseña, envía un código por email al usuario y solicita ese código en una página de verificación. Tras validar el código se completa la sesión sin volver a pedir la contraseña.

Resumen del flujo de trabajo

  • El usuario envía usuario y contraseña en la pantalla de login estándar.
  • WordPress valida las credenciales si son correctas, el plugin genera un token código, lo guarda temporalmente y envía el código por email.
  • El plugin redirige al usuario a una página de verificación (por ejemplo /wp-2fa-verify/?token=…).
  • El usuario introduce el código recibido por email. El plugin verifica el código y, si es válido, completa el inicio de sesión con wp_set_auth_cookie y redirige al usuario a su destino.

Consideraciones de seguridad y buenas prácticas

  • HTTPS obligatorio: Siempre debe ejecutarse sobre HTTPS para proteger las cookies y datos de sesión.
  • No almacenar contraseñas: El sistema propuesto no guarda contraseñas en texto. La autenticación final se realiza mediante funciones de WordPress tras verificar el 2FA.
  • Caducidad corta: El código debe expirar (ej. 5-10 minutos) y debe eliminarse/borrarse tras uso o tras superar intentos permitidos.
  • Límites de intento: Implementar controles para evitar ataques por fuerza bruta (contadores por token o por usuario).
  • Correo fiable: Asegúrate de que WordPress puede enviar correos correctamente (usar SMTP si hace falta).
  • Roles y excepciones: Puedes permitir desactivar 2FA para usuarios con ciertos meta o roles (ej. cuentas de servicio).

Implementación: plugin completo

El siguiente código es un plugin listo para colocar en wp-content/plugins/wp-email-2fa/wp-email-2fa.php. Cópialo, activa el plugin desde el administrador y prueba el flujo de login.

 Verificación 2FA,
        post_name    => slug,
        post_content => Página para verificar el código 2FA enviado por correo.,
        post_status  => publish,
        post_type    => page,
    ) )
}

/
  Interceptar el flujo de autenticación después de la comprobación de usuario/clave.
  Se engancha con prioridad mayor que la comprobación por defecto (20), por ejemplo 30.
 /
add_filter( authenticate, wp_email_2fa_intercept_auth, 30, 3 )
function wp_email_2fa_intercept_auth( user, username, password ) {
    // Si ya hay un error o la entrada no es un WP_User, devolver tal cual.
    if ( is_wp_error( user )  ! is_a( user, WP_User ) ) {
        return user
    }

    // Aquí ya sabemos que usuario y contraseña son correctos.
    user_id = user->ID

    // Generar token y código
    try {
        raw_token = bin2hex( random_bytes( WP_EMAIL_2FA_TOKEN_LENGTH ) )
    } catch ( Exception e ) {
        // Fallback
        raw_token = bin2hex( openssl_random_pseudo_bytes( WP_EMAIL_2FA_TOKEN_LENGTH ) )
    }
    code = random_int( 100000, 999999 ) // 6 dígitos

    transient_key = wp_email_2fa_ . raw_token
    data = array(
        user_id  => user_id,
        code     => (string) code,
        expires  => time()   WP_EMAIL_2FA_CODE_TTL,
        attempts => 0,
    )
    set_transient( transient_key, data, WP_EMAIL_2FA_CODE_TTL )

    // Preparar y enviar el correo
    to = user->user_email
    subject = sprintf( [%s] Código de verificación (2FA), get_bloginfo( name ) )
    message  = Hola  . user->user_login . ,nn
    message .= Se ha solicitado un inicio de sesión. Usa el siguiente código para completar el acceso:nn
    message .= code . nn
    message .= Este código expirará en  . (WP_EMAIL_2FA_CODE_TTL / 60) .  minutos.nn
    message .= Si no fuiste tú, ignora este mensaje.nn
    message .= IP:  . _SERVER[REMOTE_ADDR] . n
    message .= Fecha:  . date_i18n( get_option( date_format ) .   . get_option( time_format ) ) . n

    // Enviar correo (usa wp_mail) recomendable usar plugin SMTP en entornos en producción.
    wp_mail( to, subject, message )

    // Redirigir al usuario a la página de verificación con token
    verify_url = site_url( /wp-2fa-verify/?token= . rawurlencode( raw_token ) )
    wp_redirect( verify_url )
    exit // detener el flujo normal: no iniciar sesión hasta verificar el código
}

/
  Manejar la página de verificación (template_redirect).
  Mostramos un formulario que recibe token y código al validar, se inicia sesión.
 /
add_action( template_redirect, wp_email_2fa_render_verify_page )
function wp_email_2fa_render_verify_page() {
    if ( ! is_page( wp-2fa-verify ) ) {
        return
    }

    // Forzar no-cache
    nocache_headers()

    // Si llega formulario POST
    if ( _SERVER[REQUEST_METHOD] === POST ) {
        token = isset( _POST[wp_2fa_token] ) ? sanitize_text_field( wp_unslash( _POST[wp_2fa_token] ) ) : 
        code  = isset( _POST[wp_2fa_code] ) ? sanitize_text_field( wp_unslash( _POST[wp_2fa_code] ) ) : 
    } else {
        token = isset( _GET[token] ) ? sanitize_text_field( wp_unslash( _GET[token] ) ) : 
        code = 
    }

    message = 
    success = false

    if ( token ) {
        transient_key = wp_email_2fa_ . token
        data = get_transient( transient_key )

        if ( ! data ) {
            message = Token inválido o expirado. Vuelve a iniciar sesión para recibir un nuevo código.
        } else {
            // Si POST: procesar código
            if ( _SERVER[REQUEST_METHOD] === POST ) {
                // Verificar intentos
                if ( ! isset( data[attempts] ) ) {
                    data[attempts] = 0
                }

                if ( data[attempts] >= WP_EMAIL_2FA_MAX_ATTEMPTS ) {
                    // Bloquear token
                    delete_transient( transient_key )
                    message = Has superado el número máximo de intentos. Vuelve a iniciar sesión para generar un nuevo código.
                } elseif ( time() > data[expires] ) {
                    delete_transient( transient_key )
                    message = El código ha caducado. Inicia sesión de nuevo para recibir uno nuevo.
                } elseif ( hash_equals( data[code], code ) ) {
                    // Código correcto: finalizar login
                    user_id = intval( data[user_id] )
                    delete_transient( transient_key )

                    // Iniciar sesión programáticamente
                    wp_set_current_user( user_id )
                    wp_set_auth_cookie( user_id )
                    do_action( wp_login, wp_get_current_user()->user_login, wp_get_current_user() )

                    // Redirigir al destino: si se pasó redirect_to, usarlo sino al admin_dashboard o home
                    redirect = admin_url()
                    if ( isset( _POST[redirect_to] )  ! empty( _POST[redirect_to] ) ) {
                        redirect_candidate = esc_url_raw( wp_unslash( _POST[redirect_to] ) )
                        redirect = redirect_candidate
                    } elseif ( isset( _GET[redirect_to] )  ! empty( _GET[redirect_to] ) ) {
                        redirect = esc_url_raw( wp_unslash( _GET[redirect_to] ) )
                    } else {
                        // preferir home si no es admin
                        redirect = admin_url()
                    }

                    wp_safe_redirect( redirect )
                    exit
                } else {
                    // Código incorrecto: incrementar intentos y actualizar
                    data[attempts] = intval( data[attempts] )   1
                    set_transient( transient_key, data, data[expires] - time() )
                    remaining = WP_EMAIL_2FA_MAX_ATTEMPTS - data[attempts]
                    message = Código incorrecto. Intentos restantes:  . remaining
                }
            }
        }
    } else {
        message = No se ha encontrado token. Inicia sesión primero y te redirigiremos aquí con un token.
    }

    // Mostrar la página: usar salida básica (sin dependencias de plantillas)
    // NOTA: Aquí se imprime HTML simple. El sitio puede sobrescribir plantilla si se desea.
    ?>
    

Verificación en dos pasos (2FA)

Introduce el código de 6 dígitos que has recibido por correo.

> } ?>


>Volver al inicio de sesión

Explicación del código

  1. Activación: El plugin crea una página con slug wp-2fa-verify para mostrar el formulario de verificación.
  2. Interceptación de login: Usamos add_filter(authenticate, …) con prioridad 30. Si la comprobación de credenciales devuelve un WP_User válido, el plugin genera un token y un código de 6 dígitos, guarda estos datos en un transient y envía el código por correo. A continuación redirige al usuario a la página de verificación y detiene el flujo normal para que no se complete el login.
  3. Token almacenado en transient: La entrada temporal contiene user_id, code, expiry y contador de intentos. El uso de transient evita que quede almacenamiento persistente innecesario y expira automáticamente.
  4. Página de verificación: En template_redirect detectamos la página y procesamos POST con token y código. Si la comprobación es correcta se llama a wp_set_auth_cookie y se completa el inicio de sesión. Si no, se contabilizan intentos y se dan mensajes de error.

Personalizaciones y mejoras sugeridas

  • Añadir logging de intentos fallidos para alertas administrativas.
  • Enviar enlace en vez de código (one-click link) si se usa enlace, firmar el token con HMAC para evitar manipulación.
  • Soporte para plantillas: en lugar de imprimir HTML desde template_redirect, usar una plantilla de página dedicada o shortcodes.
  • Permitir que usuarios activen/desactiven 2FA mediante user meta y una UI en perfil de usuario.
  • Integrar con proveedores SMTP (WP Mail SMTP) para mejorar la entregabilidad.
  • Limitar 2FA por rol: por ejemplo forzar 2FA sólo para administradores.
  • Requerir nonces y sanitización completa (ya se muestra un ejemplo básico con sanitización).

Pruebas y verificación

  1. Sube el archivo del plugin a wp-content/plugins/wp-email-2fa/ y actívalo desde el panel de WordPress.
  2. Visita la pantalla de login, introduce credenciales válidas. Deberías ser redirigido a /wp-2fa-verify/?token=…
  3. Revisa el buzón del usuario: deberías recibir un correo con el código de 6 dígitos.
  4. Introduce el código y verifica que se completa la sesión y te redirige al área correspondiente.
  5. Prueba códigos incorrectos, expiraciones y número máximo de intentos para comprobar que las restricciones funcionan.

Errores comunes y cómo solucionarlos

  • No llega el correo: Comprueba que WordPress puede enviar correos. Instala y configura un plugin SMTP si es necesario.
  • Token expirado inmediatamente: Verifica que la hora del servidor sea correcta y que WP_EMAIL_2FA_CODE_TTL no sea demasiado corta.
  • Redirecciones infinitas: Asegúrate de que la lógica de redirección sólo se ejecuta dentro del filtro authenticate cuando credenciales son válidas el plugin redirige y termina la ejecución con exit para evitar loops.
  • Problemas con caches: Asegúrate de que la página de verificación no sea cacheada por el servidor o CDN.

Notas finales

Este sistema es una implementación básica y útil para añadir una capa extra de seguridad sin depender de apps de autenticación externas. Para entornos críticos o producción con muchos usuarios se recomienda emplear soluciones consolidadas, usar proveedores de envío de correo fiables y ampliar el plugin con características de seguridad adicionales (registro de intentos, bloqueo por IP, protección contra enumeración de cuentas, soporte para códigos vía SMS o app, etc.).

Referencias rápidas

  • Funciones clave de WordPress: authenticate, wp_mail, set_transient/get_transient, wp_set_auth_cookie.
  • Comprobar configuración de correo: usar plugin SMTP (por ejemplo WP Mail SMTP).


Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *