← All posts

Un proveedor de identidad nos dijo quién eras, y le creímos

Authagonal·July 6, 2026
authsecurityssosamloidcfederation

El inicio de sesión único tiene una premisa silenciosa: cuando un usuario llega desde un proveedor de identidad, ese proveedor ya ha respondido por él, así que puedes saltarte la contraseña y simplemente dejarlo entrar. Todo el sentido está en confiar en el IdP. El error que estamos a punto de describir vivía en la brecha entre «confía en que el IdP autentique a sus propios usuarios» y «confía en cualquier cosa que el IdP te diga sobre quiénes son sus usuarios». No son la misma frase, y la diferencia fue una toma de control de cuenta.

Este es el escenario. Un inquilino en nuestra plataforma puede configurar conexiones federadas: SAML hacia su IdP corporativo, OIDC hacia otro. Un usuario inicia sesión a través de una de esas conexiones, el IdP devuelve una aserción, y nuestro servidor tiene que responder a una pregunta: ¿qué cuenta local es esta? Equivócate en ese emparejamiento y tendrás o bien a un desconocido bloqueado fuera de su propia cuenta o, mucho peor, a un desconocido entrando en la de otra persona.

La clave de unión era un correo, y un correo no es más que una afirmación

Nuestro código respondía a «qué cuenta es esta» de la manera obvia. La aserción portaba un correo, así que buscábamos al usuario por él: FindByEmailAsync(assertedEmail). Si existía una cuenta coincidente, el usuario federado recurrente se resolvía sobre ella y se iniciaba su sesión. Limpio, simple, y exactamente como están escritas muchas integraciones de SSO.

El problema es lo que ese correo realmente es. No es un hecho que el IdP haya demostrado. Es una cadena de texto dentro de una aserción, y la aserción es tan confiable como la conexión por la que llegó. En un mundo multiinquilino no controlas todas las conexiones. Cualquiera que pueda montar una (su propio IdP SAML, su propio proveedor OIDC) puede poner la cadena que quiera en el campo del correo. Así que una conexión en la que no confías afirma email = [email protected], nuestra búsqueda encuentra la cuenta real del director ejecutivo, y el atacante queda con la sesión iniciada como él. Sin contraseña, sin phishing, sin fallo en la criptografía. La firma de la aserción era perfectamente válida. Simplemente respondía por una afirmación que jamás deberíamos haber tratado como una identidad.

Lo inquietante es que cada pieza individual funcionaba. El IdP autenticó correctamente a su propio usuario. La firma se verificó. La cuenta existía. La toma de control no fue el fallo de ninguna comprobación; fue usar el campo equivocado como clave.

Por qué más validación no es la solución

La reacción tentadora es validar el correo con más dureza. Exigir email_verified. Comprobar el dominio. Añadir una regla. Pero fíjate en la forma de la trampa: el atacante controla la aserción, así que cualquier propiedad que leas de la aserción es una propiedad que el atacante puede establecer. Exigir email_verified = true de una conexión que el atacante posee es como poner al zorro a custodiar el gallinero. No puedes validar tu salida de confiar en la fuente equivocada.

El correo está bien como comodidad. Es una clave primaria terrible, porque es global (la misma dirección puede aparecer en muchos proveedores) y falsificable (es lo que diga la conexión que la afirma). Una clave de unión no debe ser ninguna de las dos cosas.

La solución fue cambiar la clave, no añadir comprobaciones

La verdadera solución fue dejar de unir por el correo en absoluto y unir por lo único que una conexión no puede falsificar para un proveedor que no posee: el par (provider, sub). El sub es el identificador de sujeto que el proveedor emite para ese usuario, acotado a ese proveedor. Un usuario recurrente se resuelve buscando la identidad local previamente vinculada al (provider, sub) de esta conexión. La conexión de un atacante tiene su propio id de proveedor; puede acuñar cualquier sub que quiera dentro de su propio espacio de nombres, pero no puede producir el (provider, sub) de la conexión de otra persona. El espacio de nombres es el foso.

El correo cae entonces al papel que siempre debió tener: una pista para vincular, nunca para resolver, y solo cuando hay quien responda por él. Hacemos coincidir un correo afirmado con una cuenta existente únicamente cuando el correo está verificado por una fuente en la que realmente confiamos para ese dominio: email_verified proveniente de una conexión cuyo dominio figura en los AllowedDomains del inquilino. Fuera de eso, una nueva identidad federada obtiene su propia cuenta, no la de otra persona.

La lección, extraída

Cuando federas la identidad, separa dos preguntas que te ves tentado a fundir. «¿Autenticó este proveedor a este usuario?» la responde la firma. «¿Quién es este usuario en mi sistema?» tiene que responderla una clave que la parte que afirma no pueda falsificar a través de las fronteras, lo que significa un sujeto propio del proveedor, no un atributo global como el correo. Trata cada campo de una aserción como controlado por el atacante a menos que la fuente concreta de ese campo sea una en la que confíes para esa afirmación concreta. El correo es el campo al que la gente recurre primero porque es humano y parece único. Es exactamente el equivocado.

Esto lo lanzamos como parte de una pasada de seguridad previa al lanzamiento sobre nuestro propio servidor de autenticación, antes de que nadie nos confiara sus inicios de sesión. Es el tipo de error que pasa todas las pruebas, verifica todas las firmas, y entrega las llaves a quien las pida en el formato correcto. La solución no fue una cerradura mejor. Fue darnos cuenta de que habíamos estado leyendo la línea equivocada del documento de identidad.

Federar la identidad de forma segura se reduce a unir por una clave que la parte que afirma no pueda falsificar, y equivocarse en esto es una toma de control de cuenta que pasa todas las pruebas. Authagonal resuelve a los usuarios federados por su sujeto propio del proveedor, nunca por el correo contenido en la aserción.