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 🙂 |
