BookinglyTech News
Ciberseguridad

Suspender a un usuario en Kinde no corta el token que ya tiene su agente de IA

Un access token de OAuth es una credencial firmada que nadie consulta contra una base de datos, así que la baja del usuario no llega hasta ella. Esta demo mide el hueco y propone cómo taparlo.

3 min de lecturaDev.to0 vistas

Un access token de OAuth no es un recibo que se devuelve para contrastar contra un libro mayor. Es una afirmación firmada: un JSON pequeño con una firma criptográfica al lado, y quien lo valida comprueba esa firma contra una clave pública en lugar de preguntar a nadie si el token sigue vigente. De ahí sale todo lo demás.

La consecuencia práctica es la que documenta esta demostración: si se suspende o se borra a un usuario en Kinde, su agente sigue trabajando con el token que ya tenía en la mano. La suspensión cambia una fila en la base de datos de Kinde, pero no alcanza al token emitido y firmado antes. El agente no tiene forma de enterarse de que algo cambió. El autor lo comprobó suspendiendo a un usuario a media tarea y viendo cómo el bucle seguía adelante; su propia comprobación seguía reportando el token como válido segundos después.

Eso no es un fallo de Kinde ni de OAuth. Es lo que es una credencial sin estado, por diseño.

El punto por el que pasa todo

El montaje es una app pequeña: un bucle sobre la API de Messages de Claude con tres herramientas cerradas (list_resources, read_resource y write_resource) contra un conjunto de recursos de demo, Convex detrás para guardar el estado del usuario y Kinde para el inicio de sesión. La parte interesante no es lo que hace el agente, sino la costura única por la que pasa cada llamada antes de ejecutarse.

Esa costura son dos piezas. La primera es un webhook: Kinde emite un evento firmado en user.updated y user.deleted, el receptor verifica la firma y actualiza el registro propio de la aplicación. La segunda es una comprobación que corre antes de cada acción del agente y lee ese registro propio, en vez de fiarse de lo que era cierto cuando arrancó la sesión.

Por debajo hay un registro de acciones, ACTION_REGISTRY, que define las tres herramientas en una tabla cerrada. Tanto el esquema que se le entrega al modelo como la comprobación de permisos se construyen desde esa misma tabla, así que no pueden divergir con el tiempo como suele pasar cuando el esquema y la lista de permisos viven en sitios distintos y alguien se olvida de actualizar uno.

La decisión final está en una función pura, decideAccess, con dos modos. El modo ingenuo permite todo sin mirar el estado del usuario: es la vulnerabilidad reproducida a propósito, para poder ejecutar ambos modos contra el mismo código. El modo estricto permite un único caso, un usuario activo confirmado, y rechaza el resto. Un estado que la lectura no consigue resolver tampoco se salva.

El repositorio de demostración tiene el código completo, y la documentación de webhooks de Kinde cubre el formato de los eventos firmados.

Durante las pruebas apareció además un fallo que podía dejar el registro de un usuario dado de baja marcado como activo para siempre, un recordatorio de que la parte frágil no es el token sino el camino que va del proveedor de identidad a tu propio almacén.

Lo que importa para quien despliega agentes que actúan en nombre de otras personas es la ventana. Entre la baja del usuario y la expiración natural del token hay un intervalo en el que el agente sigue ejecutando acciones destructivas con permisos completamente válidos. El token sin estado ahorra una consulta por petición; la contrapartida es que la revocación hay que construirla encima, y no viene con el proveedor.