Como crear enlaces firmados para reset de contraseñas personalizados en WordPress

Contents

Introducción

En WordPress, el sistema nativo de restablecimiento de contraseñas funciona correctamente para la mayoría de los casos, pero a veces necesitamos generar enlaces de restablecimiento personalizados: por ejemplo, enlaces enviados desde un sistema externo, para flujos de onboarding, enlaces con branding personalizado o para enlaces de una sola vez con control preciso sobre caducidad y uso. Este artículo explica en detalle cómo crear enlaces firmados criptográficamente para restablecer contraseñas en WordPress, cómo verificarlos de forma segura y cómo integrarlos en una ruta personalizada. Incluye consideraciones de seguridad, código de ejemplo y prácticas recomendadas.

Concepto y amenazas

Un enlace firmado es una URL que contiene datos (por ejemplo, identificador de usuario y tiempo de expiración) y una firma HMAC. La firma se calcula con una clave secreta conocida solo por el servidor. Al recibir la URL, el servidor vuelve a calcular la firma y la compara con la recibida. Si coinciden, el contenido no ha sido alterado.

Las amenazas principales que mitigamos con enlaces firmados:

  • Alteración de parámetros (tampering): impedir que un atacante cambie el ID de usuario o la expiración.
  • Reutilización de enlaces (replay): impedir que un enlace ya utilizado pueda volver a usarse indefinidamente.
  • Exposición de la clave secreta: evitar almacenar la clave en lugares accesibles y rotarla periódicamente.

Diseño propuesto

  1. Generar un payload con datos mínimos: user_id y timestamp de expiración.
  2. Codificar el payload (base64url) y firmarlo con HMAC-SHA256 usando una clave secreta segura.
  3. Incluir en la URL los parámetros: payload y signature.
  4. Al recibir la petición, verificar firma con hash_equals, comprobar expiración y comprobar que no se haya usado antes (token single-use).
  5. Si todo es válido, generar la clave de restablecimiento nativa de WordPress (get_password_reset_key) y redirigir al formulario de restablecimiento o mostrar formulario propio.

Requisitos y consideraciones de seguridad

  • Usar HTTPS siempre.
  • Mantener la clave secreta fuera del repositorio: idealmente en wp-config.php o en variables de entorno. Ejemplo: define(SIGNED_RESET_SECRET, valor-secreto-largo)
  • Usar hash_hmac con true para obtener salida binaria y luego codificar en base64url.
  • Usar hash_equals para comparación segura contra ataques de temporización.
  • Limitar la vida útil del enlace (por ejemplo 1 hora o menos según el caso).
  • Marcar tokens como usados (user_meta o transients) para evitar reutilización.
  • Registrar intentos fallidos importantes para detección de abuso.

Funciones auxiliares: base64url

Para transmitir datos en URLs conviene usar base64url (segura para URLs, sin caracteres / ni =). Implementación en PHP:

lt?php
function base64url_encode(data) {
    return rtrim(strtr(base64_encode(data),  /, -_), =)
}

function base64url_decode(data) {
    remainder = strlen(data) % 4
    if (remainder) {
        padlen = 4 - remainder
        data .= str_repeat(=, padlen)
    }
    return base64_decode(strtr(data, -_,  /))
}
?gt

Generar el enlace firmado

La función siguiente genera una URL firmada. Usa una constante SIGNED_RESET_SECRET o una opción en la base de datos según prefieras. Se recomienda definir la clave en wp-config.php.

lt?php
function create_signed_reset_link(user_id, ttl = 3600) {
    user = get_user_by(id, user_id)
    if (!user) {
        return false
    }

    // Obtener la clave secreta (definir en wp-config.php con define(SIGNED_RESET_SECRET, ...))
    secret = defined(SIGNED_RESET_SECRET) ? SIGNED_RESET_SECRET : get_option(signed_reset_secret)
    if (empty(secret)) {
        // No recomendamos morir en producción, pero necesitas una clave.
        return false
    }

    expires_at = time()   (int) ttl
    payload = json_encode(array(
        uid => (int) user_id,
        exp => expires_at
    ))

    encoded = base64url_encode(payload)

    // Firma HMAC en binario y luego base64url
    sig_bin = hash_hmac(sha256, encoded, secret, true)
    sig = base64url_encode(sig_bin)

    // URL final — aquí se usa un endpoint que procesaremos: ?signed_reset=...sig=...
    url = add_query_arg(array(
        signed_reset => encoded,
        sig => sig
    ), home_url(/))

    return url
}
?gt

Verificar la firma y procesar el enlace

La verificación debe:

  • Decodificar el payload.
  • Comprobar expiración.
  • Recalcular la firma y comparar con hash_equals.
  • Comprobar que no se haya usado anteriormente (single-use).
lt?php
function verify_signed_payload(encoded_payload, sig_received, payload_out = null) {
    secret = defined(SIGNED_RESET_SECRET) ? SIGNED_RESET_SECRET : get_option(signed_reset_secret)
    if (empty(secret)) {
        return false
    }

    payload_json = base64url_decode(encoded_payload)
    if (payload_json === false) {
        return false
    }

    payload = json_decode(payload_json, true)
    if (!is_array(payload)  !isset(payload[uid])  !isset(payload[exp])) {
        return false
    }

    // Comprobar expiración
    if (time() > (int) payload[exp]) {
        return false
    }

    // Recalcular firma
    expected_sig_bin = hash_hmac(sha256, encoded_payload, secret, true)
    expected_sig = base64url_encode(expected_sig_bin)

    // Comparación segura
    if (!hash_equals(expected_sig, sig_received)) {
        return false
    }

    // Opcional: comprobar single-use
    user_id = (int) payload[uid]
    used_key = signed_reset_used_ . encoded_payload // identificador único por payload
    if (get_user_meta(user_id, used_key, true)) {
        return false // ya usado
    }

    payload_out = payload
    return true
}
?gt

Manejador que crea la clave de restablecimiento nativa

Una vez verificado el payload, se puede generar la clave de restablecimiento de WordPress con get_password_reset_key(user) y redirigir al flujo nativo (wp-login.php?action=rpkey=…login=…). El siguiente ejemplo añade un handler en init que procesa parámetros GET específicos.

lt?php
add_action(init, handle_signed_reset_request)
function handle_signed_reset_request() {
    if (!isset(_GET[signed_reset])  !isset(_GET[sig])) {
        return
    }

    encoded = sanitize_text_field(_GET[signed_reset])
    sig = sanitize_text_field(_GET[sig])

    payload = null
    if (!verify_signed_payload(encoded, sig, payload)) {
        wp_die(Enlace inválido o caducado, Error, array(response =gt 400))
    }

    user_id = (int) payload[uid]
    user = get_user_by(id, user_id)
    if (!user) {
        wp_die(Usuario no encontrado, Error, array(response =gt 404))
    }

    // Marcar el token como usado (single-use)
    used_key = signed_reset_used_ . encoded
    update_user_meta(user_id, used_key, time())

    // Generar la clave de restablecimiento de WP (usa correo si el usuario está registrado)
    reset_key = get_password_reset_key(user)
    if (is_wp_error(reset_key)) {
        wp_die(No se pudo generar la clave de restablecimiento, Error, array(response =gt 500))
    }

    // Redirigir al formulario nativo de WordPress con la clave y login
    rp_url = wp_login_url() // normalmente /wp-login.php
    rp_url = add_query_arg(array(
        action =gt rp,
        key =gt rawurlencode(reset_key),
        login =gt rawurlencode(user->user_login)
    ), rp_url)

    wp_redirect(rp_url)
    exit
}
?gt

Ejemplo completo: generar y enviar enlace por email

Ejemplo de uso para generar la URL y enviarla por correo:

lt?php
// Generar enlace para el usuario 123 y enviarlo por email
user_id = 123
link = create_signed_reset_link(user_id, 3600) // 1 hora

if (link) {
    user = get_user_by(id, user_id)
    subject = Restablece tu contraseña
    message = Haz clic en el siguiente enlace para restablecer tu contraseña:  . link
    wp_mail(user->user_email, subject, message)
}
?gt

Opciones avanzadas y mejoras

  • Single-use por clave independiente: en vez de usar el payload codificado como índice, puedes generar un token aleatorio (UUID) y almacenarlo en user_meta con asociación a usuario y expiración firmar solo con el token en el payload. Esto permite invalidar tokens fácilmente.
  • Reputación y rate limiting: limitar envío de enlaces por IP/usuario para evitar abusos.
  • Rotación de claves: cambiar SIGNED_RESET_SECRET periódicamente si la rotación ocurre, los enlaces anteriores dejarán de ser válidos (planificar según necesidad).
  • Registrar eventos: auditar cuando se generan y consumen tokens para detectar patrones sospechosos.
  • Uso de nonce o id de petición: para evitar replay incluso antes de marcar usado: almacenar un registro temporal (transient) con la combinación UID payload.

Consideraciones prácticas y compatibilidad

Este método es compatible con instalaciones WordPress estándar y aprovecha get_password_reset_key para integrarse con el flujo nativo. Si usas un sistema de autenticación externo (SSO) o un front-end headless, puedes adaptar el handler para que devuelva una respuesta JSON y permita al front-end presentar un formulario de restablecimiento propio.

Asegúrate de probar todo en un entorno staging: generación, expiración, reutilización, manejo de errores y compatibilidad con plugins de seguridad que podrían bloquear parámetros personalizados.

Resumen de parámetros en la URL

signed_reset Payload base64url con uid y exp
sig Firma HMAC-SHA256 del payload (base64url)

Conclusión

Crear enlaces firmados para restablecer contraseñas en WordPress aporta control sobre expiración, uso único y validación robusta contra manipulación. Implementaciones seguras requieren una clave secreta bien protegida, comprobación de firma con hash_equals, HTTPS obligatorio y mecanismos para evitar reutilización. El patrón mostrado combina simplicidad y compatibilidad con las funciones nativas de WordPress, y puede adaptarse a flujos personalizados o APIs.



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 *