Por qué el acceso social duplica cuentas y cómo lo arregla una tabla de bloqueo
- Publicado el
- • 4 min de lectura•--- vistas
El acceso social parece un problema resuelto. El proveedor redirige de vuelta con un código, lo intercambias por un perfil, buscas o creas un usuario, lo autenticas. Cuatro pasos, todos documentados.
Y entonces soporte empieza a fusionar clientes duplicados a mano.
El fallo
Una llamada de retorno de autorización es una petición HTTP corriente, y las peticiones HTTP corrientes ocurren más de una vez. El usuario pulsa dos veces el botón de confirmar del proveedor. Una conexión móvil inestable hace que el navegador reintente. Un precargador de enlaces calienta la URL antes de que el dedo termine el gesto. Cualquiera de estos casos entrega la misma llamada dos veces, con milisegundos de diferencia.
Ambas peticiones llevan el mismo código de autorización. Ambas lo intercambian. Ambas buscan al usuario por correo. Ninguna encuentra nada, porque ninguna ha terminado de escribir todavía. Ambas crean una cuenta.
Ahora tienes dos clientes con la misma dirección de correo. Si tu proveedor no devuelve correo, tienes dos cuentas con nombres de acceso casi idénticos y ninguna forma de saber cuál guarda el historial de pedidos.
Por qué el arreglo obvio no arregla nada
El instinto es proteger la escritura:
if (!email_exists($email)) {
wp_insert_user([...]);
}
Esto no ayuda, y entender por qué es justamente lo importante. La comprobación y la escritura son dos operaciones separadas contra la base de datos, y nada impide que la segunda petición ejecute su comprobación en el hueco entre la comprobación y la escritura de la primera. La ventana es pequeña. No es cero, y las llamadas llegan exactamente en las ráfagas que la encuentran.
Un índice único sobre la columna de correo sí es una mejora real: la base rechazará la segunda inserción. Pero convierte un problema de datos en una página de error: ahora falla la segunda petición, que es precisamente la que el navegador del usuario está mirando. Ve un error después de un acceso correcto.
Reservar el código, no el usuario
La clave está en que lo genuinamente único aquí no es el usuario. Es el código de autorización. Los proveedores emiten un código por intento de acceso, y es de un solo uso por diseño.
Así que resérvalo antes de hacer cualquier otra cosa:
public function acquire(string $code): bool
{
if ($this->checkCodeIsProcessing($code)) {
return false; // alguien ya se ocupa de este acceso
}
$this->setCodeIsProcessing($code, true);
return true;
}
El manejador de la llamada queda así:
$code = $request->get_param('code');
if (!$this->acquire($code)) {
return $this->redirectByCode($code); // seguir al ganador
}
$user = $service->authClientByCode($code);
$wp_user_id = $user->findOrCreateWpUser();
return $this->redirectByCodeAndUser($code, $wp_user_id);
La primera petición gana la reserva y hace el trabajo. La segunda pierde y, en lugar de fallar, espera a que la ganadora registre el user_id resultante y el destino de redirección, y luego emite esa misma redirección. Los dos navegadores acaban autenticados como la misma persona. Un acceso, una cuenta, ninguna página de error.
La fila de reserva lleva un contador en vez de un booleano, así que solo se elimina cuando se han atendido todas las peticiones esperadas. Eso importa cuando un proveedor llama legítimamente más de una vez.
Lo que esto no resuelve
Dos limitaciones honestas.
No es un bloqueo distribuido. checkCodeIsProcessing y setCodeIsProcessing siguen siendo dos sentencias. Sobre una única base de datos, con peticiones separadas por milisegundos, cierra la ventana en la práctica; con concurrencia real a escala conviene que la unicidad la imponga la base: un índice único sobre la columna del código, la propia inserción como reserva y una violación de restricción capturada como señal de «has perdido».
La petición que espera duerme. Seguir al ganador implica darle tiempo para terminar. Eso es una pausa fija, es decir, algo tosco: demasiado corta y el seguidor no encuentra nada, demasiado larga y un usuario espera sin motivo. Está acotada y recae solo sobre la petición perdedora, pero es un compromiso.
La forma general
Este patrón no va de OAuth. Aparece siempre que un sistema externo te llama de vuelta sin garantía de entrega exactamente una vez: notificaciones de pago, entrega de webhooks, consumidores de colas.
La regla es la misma cada vez: encuentra el identificador que el sistema externo considera único, resérvalo en un solo sitio antes de hacer trabajo alguno, y haz que el perdedor siga al ganador en lugar de fallar. Deduplicar por el resultado (el usuario, el pedido, el registro) siempre es una carrera, porque en el momento en que necesitas comprobarlo el resultado todavía no existe.
Disponible para colaboración por contrato
Estoy disponible para colaborar por contrato. Si tiene una idea de proyecto interesante, reserve una llamada por Calendly.
Agenda una llamada de 30 min