Como añadir CAPTCHA personalizado al login con PHP y JS en WordPress

Contents

Introducción

En este artículo explico, paso a paso y con todo lujo de detalles, cómo añadir un CAPTCHA personalizado al formulario de login de WordPress usando PHP y JavaScript. Te mostraré una implementación segura, compatible con la página de login por defecto (wp-login.php) y reutilizable para formularios de inicio de sesión personalizados (por ejemplo, con wp_login_form()). Incluyo code snippets completos listos para usar como plugin o integrarlos en functions.php.

Diseño de la solución

Requisitos y consideraciones de diseño:

  • Generar el CAPTCHA en el servidor (para evitar manipulación fácil).
  • No depender de sesiones PHP (compatibilidad con hosting sin session). Usamos transients para almacenar temporalmente la respuesta esperada.
  • Verificar el CAPTCHA en el hook authenticate de WordPress para detener la autenticación si falla.
  • Proveer regeneración por AJAX para mejorar UX sin recargar la página.
  • Incluir medidas básicas contra fuerza bruta (conteo de fallos por IP mediante transients).
  • Seguir buenas prácticas: sanitización, nonces para AJAX y eliminación del transient tras uso.

Resumen del flujo

  1. Al cargar el formulario de login se genera una pregunta (ej. suma simple) y se guarda la respuesta en un transient identificado por un ID único.
  2. El formulario muestra la pregunta, un campo para la respuesta y un campo oculto con el ID del CAPTCHA.
  3. Si el usuario solicita regenerar, un AJAX solicita un nuevo CAPTCHA al servidor y actualiza la UI.
  4. Al enviar el formulario, en el hook authenticate se valida la respuesta comparándola con el transient. Si no coincide, se devuelve un error y se incrementa un contador de fallos por IP.

Plugin ejemplo (archivo único)

A continuación tienes un plugin mínimo auto-contenido que puedes colocar como un archivo PHP en wp-content/plugins/captcha-login/captcha-login.php y activar en el panel. El plugin añade el CAPTCHA al login estándar y proporciona la regeneración mediante AJAX.

lt?php
/
Plugin Name: Custom Login CAPTCHA (Ejemplo)
Description: CAPTCHA personalizado para la página de login de WordPress (transients   AJAX).
Version: 1.0
Author: Ejemplo
/

/ Genera un CAPTCHA simple (suma) y guarda la respuesta en un transient /
function cl_generate_captcha() {
    a = rand(1, 9)
    b = rand(1, 9)
    answer = a   b
    id = uniqid(cl_, true) // id único para el transient
    set_transient(id, answer, 5  MINUTE_IN_SECONDS) // caduca en 5 minutos

    return array(
        id => id,
        question => sprintf(%d   %d = ?, a, b)
    )
}

/ Imprime el HTML del CAPTCHA. Útil para wp-login.php y formularios personalizados /
function cl_print_captcha() {
    c = cl_generate_captcha()
    // Nota: no hacemos echo de la respuesta, solo la pregunta y el id
    echo ltpgt
    echo ltlabel for=cl_captcha_answergt . esc_html(Resuelve: ) . esc_html(c) . lt/labelgtltbr/gt
    echo ltinput type=text name=cl_captcha_answer id=cl_captcha_answer class=input value= size=20 /gt
    echo lt/pgt
    echo ltinput type=hidden name=cl_captcha_id id=cl_captcha_id value= . esc_attr(c[id]) .  /gt
    echo ltpgtlta href=# id=cl_regenerategtRegenerar CAPTCHAlt/agtlt/pgt
}

/ Añade el CAPTCHA al formulario de login estándar /
add_action(login_form, cl_add_captcha_to_login)
function cl_add_captcha_to_login() {
    cl_print_captcha()
}

/ Validación del CAPTCHA durante la autenticación /
add_filter(authenticate, cl_check_captcha_on_authenticate, 25, 3)
function cl_check_captcha_on_authenticate(user, username, password) {
    // Solo validar si el formulario envió nuestro campo
    if ( isset(_POST[cl_captcha_id]) ) {
        cid = sanitize_text_field(wp_unslash(_POST[cl_captcha_id]))
        answer = isset(_POST[cl_captcha_answer]) ? trim(sanitize_text_field(wp_unslash(_POST[cl_captcha_answer]))) : 

        expected = get_transient(cid)
        // El transient puede haber expirado o no existir
        if (expected === false) {
            return new WP_Error(captcha_expired, ltbgtERRORlt/bgt: CAPTCHA expirado o inválido.)
        }

        // Eliminamos el transient para que no se reutilice
        delete_transient(cid)

        if ((string)expected !== (string)answer) {
            // Medida sencilla antibrute: contar fallos por IP
            ip = isset(_SERVER[REMOTE_ADDR]) ? _SERVER[REMOTE_ADDR] : unknown
            key = cl_failed_ . md5(ip)
            fails = (int) get_transient(key)
            fails  
            set_transient(key, fails, 15  MINUTE_IN_SECONDS)

            if (fails >= 6) {
                return new WP_Error(captcha_locked, ltbgtERRORlt/bgt: Demasiados intentos. Intenta de nuevo más tarde.)
            }

            return new WP_Error(captcha_incorrect, ltbgtERRORlt/bgt: CAPTCHA incorrecto.)
        }

        // Si pasó, continuar con la autenticación normal devolviendo user
    }

    return user
}

/ AJAX: regenerar CAPTCHA (para usuarios no autenticados también) /
add_action(wp_ajax_nopriv_cl_regenerate_captcha, cl_ajax_regenerate_captcha)
add_action(wp_ajax_cl_regenerate_captcha, cl_ajax_regenerate_captcha)
function cl_ajax_regenerate_captcha() {
    check_ajax_referer(cl_regenerate_captcha, nonce)

    c = cl_generate_captcha()
    wp_send_json_success(array(
        id => c[id],
        question => c
    ))
}

/ Encolado de scripts y estilos en la página de login /
add_action(login_enqueue_scripts, cl_enqueue_login_assets)
function cl_enqueue_login_assets() {
    wp_enqueue_script(cl-login-js, plugin_dir_url(__FILE__) . captcha-login.js, array(jquery), 1.0, true)
    wp_localize_script(cl-login-js, cl_ajax, array(
        ajax_url => admin_url(admin-ajax.php),
        nonce => wp_create_nonce(cl_regenerate_captcha)
    ))
    wp_enqueue_style(cl-login-css, plugin_dir_url(__FILE__) . captcha-login.css)
}
?>

Archivo JavaScript (captcha-login.js)

Este JS hace la petición AJAX para regenerar el CAPTCHA y actualiza el DOM de la página de login.

jQuery(document).ready(function(){
    (#cl_regenerate).on(click, function(e){
        e.preventDefault()
        var link = (this)
        link.text(Regenerando...)
        .ajax({
            url: cl_ajax.ajax_url,
            method: POST,
            data: {
                action: cl_regenerate_captcha,
                nonce: cl_ajax.nonce
            }
        }).done(function(r){
            if (r.success  r.data) {
                // Reemplazamos el id del hidden y la pregunta visual
                (#cl_captcha_id).val(r.data.id)
                (#cl_captcha_answer).val()
                (#cl_captcha_answer).prev(label).html(Resuelve:    r.data.question)
            } else {
                alert(Error regenerando CAPTCHA)
            }
        }).fail(function(){
            alert(Error de red al regenerar CAPTCHA)
        }).always(function(){
            link.text(Regenerar CAPTCHA)
        })
    })
})

CSS mínimo (captcha-login.css)

/ Estilos mínimos para que el CAPTCHA no rompa el layout /
#cl_captcha_answer { margin-top: 4px display: block }
#cl_regenerate { display: inline-block margin-top: 6px }

Integración en formularios personalizados

Si usas un formulario de login en el frontend (por ejemplo con wp_login_form()), solo llama a la función cl_print_captcha() donde quieras que aparezca el CAPTCHA. Asegúrate de encolar el script/css también en la página donde está el formulario. Para ello puedes usar:

// En functions.php o plugin: encolar en frontend cuando muestres tu formulario
add_action(wp_enqueue_scripts, function(){
    if ( is_page(login) ) { // o la condición que uses para la página de login
        wp_enqueue_script(cl-login-js, plugin_dir_url(__FILE__) . captcha-login.js, array(jquery), 1.0, true)
        wp_localize_script(cl-login-js, cl_ajax, array(
            ajax_url => admin_url(admin-ajax.php),
            nonce => wp_create_nonce(cl_regenerate_captcha)
        ))
        wp_enqueue_style(cl-login-css, plugin_dir_url(__FILE__) . captcha-login.css)
    }
})

// En la plantilla de la página de login:
cl_print_captcha()

Buenas prácticas y aspectos de seguridad

  • Transients: usamos transients en vez de sesiones para que funcione en entornos sin session_start(). Los transients expiran rápido (ej. 5 minutos) para evitar saturación.
  • Sanitización: siempre sanitiza los datos de _POST con sanitize_text_field() y wp_unslash() si procede.
  • Nonces para AJAX: protegen la acción de regeneración contra usos no autorizados.
  • Eliminar transient tras uso: evita reuso del mismo CAPTCHA.
  • Mecanismo anti-brute: cuentas de fallos por IP usando transient puedes mejorar con almacenamiento persistente o integrarlo con plugins de seguridad.
  • Evitar filtrar información sensible: el orden de comprobación puede influir. En este ejemplo validamos el CAPTCHA durante authenticate para detener el proceso si el CAPTCHA falla. Esto previene el consumo de recursos y reduce información revelada por errores.

Mejoras y variantes posibles

  • CAPTCHA visual (imagen). Puedes generar una imagen con GD o Imagick que muestre el texto/operación. Guarda la respuesta en transient igual que aquí.
  • Audio/accessibilidad. Proveer un enlace que genere un audio con la pregunta (text-to-speech) o alternativas accesibles para usuarios con discapacidad visual.
  • ReCAPTCHA u hCaptcha. Si prefieres una solución externa, integra sus scripts y valida en el servidor usando su API.
  • Mayor dificultad adaptativa. Subir la complejidad del CAPTCHA tras múltiples fallos desde una IP.
  • Temas de caché. Si tu página de login (o formulario en frontend) pasa por caches, asegúrate de que no se cachee el HTML del CAPTCHA (usa cabeceras o excluye la página del cache).

Depuración y problemas comunes

  1. Si al pulsar Regenerar no ocurre nada, comprueba que captcha-login.js esté correctamente encolado y que la localización cl_ajax exista.
  2. Si aparece siempre CAPTCHA expirado, revisa que los transients funcionen en tu hosting (algunos entornos deshabilitan la API de transients o requieren object-cache). Puedes sustituir transients por opciones temporales o base de datos si es necesario.
  3. Si el formulario está cacheado (ej. plugin de cache), el CAPTCHA generará siempre la misma pregunta. Excluye la página o bloque HTML del cache.
  4. Si quieres compatibilidad con plugins de seguridad que modifican el login, ajusta la prioridad del hook authenticate si hay interacciones.

Resumen final

El enfoque que propongo (pregunta simple transient verificación en authenticate AJAX para regenerar) es robusto, ligero y fácil de integrar tanto en el login por defecto como en formularios personalizados. Permite evolucionar a una imagen/audio CAPTCHA si se necesita mayor seguridad o accesibilidad. Incluye medidas básicas antibrute y respeta buenas prácticas de WordPress (nonces, sanitización, transients).



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 *