
En tiendas, cursos y comunidades, buena parte de los mensajes de soporte son «no recuerdo mi contraseña». El enlace mágico lo resuelve: la persona escribe su correo, recibe un enlace y, al hacer clic, queda dentro. Sin contraseñas que recordar.
¿Es seguro?
Tan seguro como el correo de la persona: quien controle esa bandeja puede entrar. Pero eso ya pasa hoy con «¿Olvidaste tu contraseña?», que también envía un enlace por correo. Bien implementado, el enlace mágico no agrega un riesgo nuevo.
Lo que sí cambia es que se salta el segundo factor (2FA) si lo tienes configurado. Por eso la regla de oro:
Usa el enlace mágico solo para usuarios sin permisos de edición: clientes, suscriptores, alumnos. Los administradores y editores deben seguir entrando con contraseña y 2FA, o mejor, con passkeys.
Las reglas de un enlace seguro
- Caduca pronto: 15 minutos es suficiente.
- Un solo uso: al usarlo, se borra.
- No se guarda en texto plano: en la base de datos se guarda una huella (hash) del código, no el código.
- No revela si el correo existe: siempre se muestra el mismo mensaje.
- Tiene límite: no se puede pedir un enlace cada segundo.
El mu-plugin
Agrega un enlace «Entrar con un enlace por correo» en la pantalla de acceso. Guárdalo como wp-content/mu-plugins/cwp-enlace-magico.php:
<?php
/**
* Plugin Name: Enlace mágico para clientes (Curso WordPress)
*/
const CWP_EM_VIGENCIA = 15 * MINUTE_IN_SECONDS;
// Enlace en el formulario de acceso.
add_action( 'login_form', function () {
printf( '<p style="margin-bottom:16px"><a href="%s">Entrar con un enlace por correo</a></p>', esc_url( add_query_arg( 'action', 'cwp_enlace', wp_login_url() ) ) );
} );
// Formulario para pedir el enlace y envío del correo.
add_action( 'login_form_cwp_enlace', function () {
$enviado = false;
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && check_admin_referer( 'cwp_enlace' ) ) {
$enviado = true;
$correo = sanitize_email( wp_unslash( $_POST['correo'] ?? '' ) );
$usuario = get_user_by( 'email', $correo );
// Solo usuarios sin permisos de edición, y un enlace cada 2 minutos.
if ( $usuario && ! user_can( $usuario, 'edit_posts' ) && ! get_transient( 'cwp_em_' . $usuario->ID ) ) {
$codigo = wp_generate_password( 40, false );
update_user_meta( $usuario->ID, 'cwp_em', array(
'hash' => hash_hmac( 'sha256', $codigo, wp_salt( 'auth' ) ),
'vence' => time() + CWP_EM_VIGENCIA,
) );
set_transient( 'cwp_em_' . $usuario->ID, 1, 2 * MINUTE_IN_SECONDS );
$url = add_query_arg( array( 'action' => 'cwp_entrar', 'u' => $usuario->ID, 'c' => $codigo ), wp_login_url() );
wp_mail( $usuario->user_email, 'Tu enlace para entrar a ' . get_bloginfo( 'name' ),
"Haz clic para entrar (vence en 15 minutos y sirve una sola vez):\n\n" . $url . "\n\nSi no lo pediste, ignora este correo." );
}
}
login_header( 'Entrar con un enlace' );
if ( $enviado ) {
echo '<p class="message">Si el correo está registrado, te enviamos un enlace para entrar. Revisa tu bandeja (y la carpeta de spam).</p>';
} else {
echo '<form method="post"><p><label for="correo">Tu correo</label><input type="email" name="correo" id="correo" class="input" required></p>';
wp_nonce_field( 'cwp_enlace' );
echo '<p class="submit"><input type="submit" class="button button-primary button-large" value="Enviarme el enlace"></p></form>';
}
login_footer();
exit;
} );
// Validar el enlace e iniciar sesión.
add_action( 'login_form_cwp_entrar', function () {
$id = absint( $_GET['u'] ?? 0 );
$codigo = sanitize_text_field( wp_unslash( $_GET['c'] ?? '' ) );
$datos = $id ? get_user_meta( $id, 'cwp_em', true ) : null;
delete_user_meta( $id, 'cwp_em' ); // Un solo uso, funcione o no.
$valido = is_array( $datos )
&& $datos['vence'] > time()
&& hash_equals( $datos['hash'], hash_hmac( 'sha256', $codigo, wp_salt( 'auth' ) ) )
&& ! user_can( $id, 'edit_posts' );
if ( ! $valido ) {
wp_die( 'El enlace no es válido o ya venció. Pide uno nuevo desde la pantalla de acceso.', 'Enlace no válido', array( 'response' => 403 ) );
}
wp_set_auth_cookie( $id, false );
do_action( 'wp_login', get_userdata( $id )->user_login, get_userdata( $id ) );
wp_safe_redirect( function_exists( 'wc_get_page_permalink' ) ? wc_get_page_permalink( 'myaccount' ) : home_url( '/' ) );
exit;
} );
Qué hace cada parte:
- El código del enlace tiene 40 caracteres aleatorios y en la base de datos solo se guarda su huella firmada con las claves de tu sitio.
- Al abrir el enlace, el registro se borra antes de validar: aunque el intento falle, ese código ya no sirve.
- Si es una tienda, lleva a Mi cuenta; si no, a la portada.
- Se dispara la acción
wp_login, para que los plugins de registro de actividad anoten el acceso.
Algunos programas de correo corporativo «visitan» los enlaces para revisar si son peligrosos, y eso consumiría el enlace de un solo uso. Si tus clientes reportan que el enlace «ya venció» apenas llega, la solución es que el enlace lleve a una página con un botón Entrar que haga la validación al pulsarlo. Es un buen ejercicio para extender este código.
Pruébalo
- Crea un usuario de prueba con rol Suscriptor (o Cliente en WooCommerce).
- Cierra sesión, ve a la pantalla de acceso y usa el nuevo enlace.
- Prueba también con el correo de un administrador: no debe llegar nada, y el mensaje en pantalla debe ser el mismo.
¿Tus clientes olvidan la contraseña a cada rato? Cuéntanos en el foro si probaste el enlace mágico y cómo lo recibieron.





