En la parte anterior vimos cómo gestionar usuarios con emails duplicados, uno con login tradicional y uno o más con login de Socialite.
Ahora vamos a ver cómo se acota la función del reseteo de contraseña, únicamente a usuarios de login tradicional, lo cual significa excluir a usuarios de Socialite. Prestar atención porque la solución tiene 2 partes. La primera intencionalmente es incompleta, y la segunda ya corrige y completa todo lo necesario para que realmente funcione.
Vamos a comenzar con el siguiente planteo:
La fachada Illuminate\Support\Facades\Password tiene un método sendResetLink que sirve para enviar un email de reseteo de contraseña. Si un usuario/email está repetido, una vez sin provider (usuario de login tradicional) y otra vez con provider (login con Socialite), necesito evitar que la función del email con el link se aplique al usuario de login con Socialite.
Aquí tienes la solución para evitar que el enlace de reseteo de contraseña se envíe a usuarios de Socialite, incluso si comparten el mismo email con un usuario tradicional.
La clave es interceptar la solicitud de reseteo de contraseña antes de que se llame a Password::sendResetLink
y realizar una verificación manual para asegurar que el email pertenece a un usuario tradicional (con provider
nulo).
Modificación del Controlador de Envío de Enlace de Reseteo
El controlador que maneja el envío del enlace de reseteo de contraseña es típicamente app/Http/Controllers/Auth/PasswordResetLinkController.php
si estás usando Laravel Breeze o Jetstream.
Vamos a modificar el método store
de este controlador:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User; // Importa tu modelo User
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): \Illuminate\View\View
{
return view('auth.forgot-password');
}
/**
* Handle the incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// PASO CLAVE: Buscar el usuario por email, pero solo si es una cuenta tradicional.
$user = User::where('email', $request->email)
->whereNull('provider') // <-- ¡Aquí está la restricción!
->first();
// Si no se encuentra un usuario tradicional con ese email,
// o si el email existe pero pertenece a una cuenta de Socialite,
// devolvemos un mensaje de error que no revela la existencia del email
// pero guía al usuario.
if (!$user) {
throw ValidationException::withMessages([
'email' => __('passwords.user'), // Mensaje genérico de Laravel para "no encontramos un usuario con ese email"
// O un mensaje más específico si quieres:
// 'email' => 'No podemos encontrar un usuario con esa dirección de correo electrónico o la cuenta está asociada a un inicio de sesión social.',
]);
}
// Si se encontró un usuario tradicional, procedemos con el envío del enlace de reseteo.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status === Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => trans($status),
]);
}
}
Explicación del Cambio:
Búsqueda Condicional del Usuario:
Antes de llamar a
Password::sendResetLink()
, realizamos una consulta manual a tu modeloUser
.La condición clave es
->whereNull('provider')
. Esto asegura que solo busquemos y consideremos a los usuarios que se registraron de forma tradicional (es decir, aquellos que tienen unprovider
nulo, lo que significa que no usaron Socialite).
Manejo de Errores:
Si la consulta
$user = User::where('email', $request->email)->whereNull('provider')->first();
no devuelve ningún usuario (es decir, el email no existe en tu base de datos para un usuario tradicional, o existe pero es para una cuenta de Socialite), entonces sabemos que no debemos enviar un enlace de reseteo de contraseña tradicional.En este caso, lanzamos una
ValidationException
con el mensaje__('passwords.user')
. Este es un mensaje genérico de Laravel (que puedes personalizar enlang/es/passwords.php
) que dice algo como "No podemos encontrar un usuario con esa dirección de correo electrónico". Es una buena práctica de seguridad no revelar si el email existe o no, pero en este contexto, también implica que la cuenta no es de tipo "contraseña".
Llamada a
Password::sendResetLink()
:Solo si
user
es encontrado (y, por nuestra lógica, sabemos que es un usuario tradicional), procedemos a llamar aPassword::sendResetLink()
. Este método de la fachadaPassword
hará el trabajo de generar el token, guardarlo y enviar el email.
¿Por qué este enfoque es viable y correcto?
No se modifica el núcleo de Laravel: No alteramos el comportamiento interno de
Password::sendResetLink()
. Simplemente controlamos cuándo se llama.Cumple tu regla de negocio: Asegura que solo los usuarios con un
provider
nulo (es decir, los que tienen una contraseña gestionada por tu aplicación) puedan recibir un enlace de reseteo de contraseña.Seguridad: Evita enviar enlaces de reseteo a emails que podrían estar asociados a cuentas de Socialite, lo cual no solo sería inútil (porque no tienen contraseña) sino que podría ser confuso o incluso un vector de ataque si se maneja incorrectamente.
Mensajes claros: Permite dar un feedback adecuado al usuario.
Con esta modificación, tu funcionalidad de reseteo de contraseña estará alineada con tu lógica de negocio de autenticación dual.
No está bien. Si dos usuarios tienen el mismo email, uno de login tradicional (provider nulo) y otro de Socialite (provider no nulo), mi requerimiento es que el token se genere exactamente para el usuario de login tradicional. Con la solución que me mostraste, este requerimiento no queda garantizado.
¡Tienes toda la razón! Mi solución anterior, al seguir usando Password::sendResetLink()
, no garantizaba que, en caso de emails duplicados, el token se generara específicamente para el usuario con provider
nulo. Password::sendResetLink()
realiza su propia búsqueda interna, y no podemos controlar ese filtro.
Para garantizar este requerimiento, necesitamos tomar el control total del proceso de generación del token y envío del email. Esto significa:
Buscar explícitamente al usuario tradicional (email +
provider IS NULL
).Generar el token de reseteo de contraseña manualmente para ese usuario específico.
Enviar el email de reseteo manualmente a ese usuario específico, pasándole el token generado.
Así es como lo haríamos:
Modificación del Controlador de Envío de Enlace de Reseteo
Vamos a modificar el método store
del controlador app/Http/Controllers/Auth/PasswordResetLinkController.php
.
Explicación de los Cambios y por qué Garantiza tu Requerimiento:
Búsqueda Explícita y Restringida (
User::whereNull('provider')
):La línea
$user = User::where('email', $request->email)->whereNull('provider')->first();
es el corazón de la solución.Ahora, no solo buscamos por
email
, sino que forzamos la búsqueda a encontrar solo usuarios donde la columnaprovider
seaNULL
. Esto significa que estamos buscando exclusivamente cuentas de login tradicional.Si el email
usuario@ejemplo.com
existe para un usuario tradicional Y para un usuario de Google, esta consulta solo devolverá el usuario tradicional. Si solo existe para el usuario de Google, esta consulta devolveránull
.
Generación de Token para el Usuario Específico (
Password::broker()->createToken($user)
):Una vez que hemos identificado con certeza al
$user
que es de login tradicional, llamamos aPassword::broker()->createToken($user)
.Este método genera un token de reseteo de contraseña y lo asocia específicamente con el ID de ese
$user
en la tablapassword_reset_tokens
. No hay ambigüedad.
Envío de Notificación al Usuario Específico (
Notification::send($user, ...)
):Finalmente, usamos
Notification::send($user, new ResetPasswordNotification($token))
para enviar el email.Al pasar el objeto
$user
directamente aNotification::send()
, garantizamos que el email se envíe únicamente a la dirección de correo electrónico asociada con ese objeto$user
(que sabemos que es el usuario tradicional).
¿Qué pasa si el email usuario@ejemplo.com
existe para un usuario de Socialite, pero no para un usuario tradicional?
La consulta
$user = User::where('email', $request->email)->whereNull('provider')->first();
devolveránull
.La condición
if (!$user)
se activará.Se lanzará la
ValidationException
con el mensajetrans('auth.failed')
.El usuario recibirá el mensaje genérico de error, y no se generará ni enviará ningún enlace de reseteo. Esto cumple tu requerimiento de no enviar el enlace a usuarios de Socialite.
Con esta modificación, tienes un control preciso sobre quién puede solicitar y recibir un enlace de reseteo de contraseña, garantizando que solo las cuentas tradicionales sean elegibles.
¿En el método store (done see resetea la contraseña), con el token basta para que aplique la nueva contraseña al usuario correcto?
No, el token por sí solo no es suficiente para que el método store
(en NewPasswordController
) aplique la nueva contraseña al usuario correcto. Laravel, por seguridad, requiere tanto el token como el email para verificar la solicitud de reseteo de contraseña.
Aquí te explico por qué y cómo funciona:
La Seguridad del Proceso de Reseteo de Contraseña de Laravel
Laravel implementa un proceso de reseteo de contraseña robusto que se basa en la combinación de un token único y el email del usuario para prevenir ataques y asegurar que la contraseña se cambie para la cuenta correcta.
Generación del Token (en
PasswordResetLinkController
):Cuando solicitas un reseteo, Laravel genera un token criptográficamente seguro y de un solo uso.
Este token se almacena en la tabla
password_reset_tokens
de tu base de datos, asociado alemail
del usuario y a una fecha de expiración.El token se envía al email del usuario, incrustado en la URL del enlace de reseteo (ej.
/reset-password/{token}?email=usuario@ejemplo.com
).
Verificación del Token y Email (en
NewPasswordController@store
):Cuando el usuario hace clic en el enlace y envía el formulario con la nueva contraseña, el método
store
deNewPasswordController
recibe tres datos clave:token
(de la URL)email
(del formulario, que a menudo se pre-rellena desde la URL)password
ypassword_confirmation
(la nueva contraseña)
Internamente, Laravel utiliza la fachada
Password::broker()->reset()
(o un método similar si lo has personalizado) para realizar la verificación. Este método hace lo siguiente:Busca el token en la tabla
password_reset_tokens
que coincida con eltoken
proporcionado Y elemail
proporcionado.Verifica la validez del token: Comprueba que el token no haya expirado y que no haya sido utilizado previamente.
Localiza al Usuario: Si el token es válido y coincide con el email, Laravel identifica al usuario al que pertenece ese token.
Actualiza la Contraseña: Si todas las verificaciones son exitosas, la contraseña del usuario se actualiza en la base de datos.
Elimina el Token: El token se elimina de la tabla
password_reset_tokens
para asegurar que no pueda ser reutilizado.
¿Por qué se necesita el email además del token?
Capa Adicional de Seguridad: Si un atacante lograra obtener un token (por ejemplo, a través de un ataque de phishing muy sofisticado o un error de registro), el requerimiento del email añade una capa extra de protección. El atacante necesitaría tanto el token como el email para completar el reseteo.
Manejo de Emails Duplicados (Tu Caso): En tu escenario específico donde los emails pueden estar duplicados (un usuario tradicional y uno social con el mismo email), el
email
en la URL del token es crucial.Aunque en el
PasswordResetLinkController
nos aseguramos de que el token se genere solo para el usuariowhereNull('provider')
, el proceso dePassword::broker()->reset()
aún necesita el email para buscar en la tablapassword_reset_tokens
y luego identificar al usuario.La combinación
token + email
asegura que, incluso si hay múltiples entradas con el mismo email en la tablausers
, el reseteo se aplique al usuario correcto, ya que el token está vinculado a la entrada específica enpassword_reset_tokens
que, a su vez, está vinculada al email del usuario tradicional.
Conclusión
Sí, en el método store
del controlador de reseteo de contraseña, el token
y el email
son ambos necesarios para que Laravel pueda identificar de forma segura al usuario correcto y aplicar la nueva contraseña. El token es el identificador único de la solicitud de reseteo, y el email actúa como una verificación adicional y un medio para localizar la entrada correcta en la tabla de tokens de reseteo.
No hay comentarios:
Publicar un comentario