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
- Generar un payload con datos mínimos: user_id y timestamp de expiración.
- Codificar el payload (base64url) y firmarlo con HMAC-SHA256 usando una clave secreta segura.
- Incluir en la URL los parámetros: payload y signature.
- Al recibir la petición, verificar firma con hash_equals, comprobar expiración y comprobar que no se haya usado antes (token single-use).
- 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 🙂 |