El inicio de sesión OIDC a través de la aplicación Discourse iOS falla ocasionalmente con csrf_detected en la devolución de llamada

Hola,

Estoy ejecutando Discourse (2026.2.0-latest (f7cec86997)) con OpenID Connect (Azure / Entra ID como IdP).

He notado un fallo de inicio de sesión ocasional que solo parece ocurrir cuando los usuarios intentan iniciar sesión a través de la aplicación Discourse para iOS.

A partir de los registros del servidor, el flujo se ve así:

POST /auth/oidc
GET  /auth/oidc/callback?...state=...
(oidc) Authentication failure! csrf_detected

La devolución de llamada llega a Discourse, pero la validación CSRF/state falla, por lo que no se crea ninguna cuenta de usuario.

Los registros circundantes sugieren que esto está sucediendo en el flujo de transferencia de la aplicación:
application_name=Discourse - iPhone
auth_redirect=discourse://auth_redirect

Desde la perspectiva del usuario, no aparece nada obvio: simplemente se les devuelve a la pantalla de inicio de sesión y, a menudo, no recuerdan haber visto un error.

Esto no parece ocurrir al iniciar sesión a través de Safari o navegadores de escritorio.

Mi suposición es que esto está relacionado con la partición de cookies de iOS / el cambio de contexto entre el navegador dentro de la aplicación y la devolución de llamada de la aplicación.

Solo quería verificar:
• si este es el comportamiento esperado con OIDC + la aplicación iOS
• y si hay alguna mitigación recomendada además de asegurar un origen canónico HTTPS estricto

Gracias; estaré encantado de proporcionar fragmentos de registro anonimizados si son útiles.

Punto de datos adicional de los registros de acceso de nginx:

Un fallo representativo (2026-01-25 11:44:10 UTC) muestra que la solicitud de devolución de llamada OIDC proviene de un agente de usuario de un navegador dentro de la aplicación de iOS (Snapchat), no del agente de usuario del webview de la aplicación de Discourse para iOS:

GET /auth/oidc/callback?...state=... 302
UA: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) ... Snapchat/13.76.1.0 (like Safari/..., panda)
Referer: https://login.microsoftonline.com/

Inmediatamente seguido por:
GET /auth/failure?message=csrf_detected&strategy=oidc

Por lo tanto, parece que el flujo OAuth a veces se inicia dentro de un navegador dentro de la aplicación de iOS (Snapchat/otro), luego ocurre el traspaso (también he visto registros que contienen auth_redirect=discourse://auth_redirect), y la cookie de sesión/estado no sobrevive de manera consistente.
Configuración actual: SiteSetting.same_site_cookies = "Lax".

Pregunta: ¿Se espera que el flujo de autenticación de la aplicación móvil de Discourse sea fiable cuando el inicio de sesión se inicia desde navegadores dentro de la aplicación de iOS que luego enlazan profundamente a la aplicación de Discourse?
¿Sería cambiar same_site_cookies a “None” la mitigación recomendada, o hay un enfoque mejor?

Tras una investigación adicional y confirmación del uso en el mundo real.

Tras investigar más a fondo, creo que esto es, de hecho, una limitación del navegador integrado de iOS (WKWebView) en lugar de algo específico de Discourse o de la configuración de Azure.

Lo que he podido confirmar

A partir de los registros de nginx + Rails y las pruebas de usuario:

  • El flujo de OAuth a veces se inicia dentro de un navegador integrado de iOS (por ejemplo, Snapchat)
  • El inicio de sesión de Microsoft (login.microsoftonline.com) se carga dentro de ese navegador integrado
  • La devolución de llamada OIDC llega a Discourse con éxito
  • Pero la cookie de sesión que contiene el valor de estado no sobrevive
  • Lo que resulta en un:
GET /auth/failure?message=csrf_detected&strategy=oidc

determinista

Esto ocurre a pesar de que el referente es Microsoft y la URI de redirección es correcta.

Un UA representativo:

Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X)
Snapchat/13.76.1.0 (like Safari/..., panda)

Así que, aunque la interfaz de usuario “uniboxea” bien a Microsoft, el navegador subyacente sigue siendo el WKWebView de Snapchat, no Safari / ASWebAuthenticationSession.

La intercepción de componentes de tema no funciona

Intenté mitigar esto del lado del cliente usando un componente de tema:

  • detectando UAs de navegadores integrados conocidos
  • bloqueando enlaces /auth/oidc
  • mostrando una superposición persistente que instruye a los usuarios a abrir en Safari/Chrome

Sin embargo, esto no intercepta el flujo de manera confiable, porque:

  • la redirección de OAuth se inicia del lado del servidor
  • la cookie de estado debe existir antes de que se ejecute el JS del cliente
  • cuando el tema se carga, el daño ya está hecho

Por lo tanto, un componente de tema no puede prevenir de manera confiable este modo de fallo.

Lo que funciona de manera confiable

La única mitigación que he encontrado que funciona consistentemente es anular el texto del sitio:

login.omniauth_error.csrf_detected

para explicar explícitamente que:

  • el inicio de sesión falló debido a un navegador integrado
  • los usuarios deben abrir el sitio en Safari o Chrome
  • y luego reintentar el inicio de sesión

Este mensaje se renderiza del lado del servidor después del fallo y, por lo tanto, aparece incluso en contextos de navegador integrado rotos.

Eso ha reducido significativamente la confusión del usuario, ya que anteriormente los usuarios eran devueltos silenciosamente a la página de inicio de sesión y, a menudo, no se daban cuenta de que algo había salido mal.

Acerca de las cookies SameSite

No he cambiado same_site_cookies a “None”.

Dado que este es un problema de aislamiento de WKWebView (en lugar de navegación entre sitios en Safari), cambiar SameSite no parece abordar la causa raíz y puede introducir compensaciones de seguridad innecesarias.

Pregunta abierta

Dado lo anterior, quería verificar si:

  • este comportamiento se considera esperado cuando OAuth se inicia desde navegadores integrados de iOS
  • y si Discourse tiene la intención de admitir ese flujo, o simplemente documentar que el inicio de sesión debe realizarse desde un navegador real

También podría ser útil para Discourse mostrar un mensaje predeterminado más explícito para csrf_detected en contextos de OmniAuth, ya que este modo de fallo parece ser cada vez más común con los usuarios estudiantes que llegan a través de enlaces de Snapchat / Instagram.

Estoy dispuesto a proporcionar más registros anonimizados si son útiles, pero en este momento el comportamiento parece muy consistente y reproducible.

Gracias por echarle un vistazo.

Un punto de datos adicional que puede ayudar a confirmar que se trata de una limitación determinista de WKWebView en lugar de un comportamiento intermitente.

Al correlacionar los registros de acceso de nginx en múltiples fallos, he descubierto que el mismo valor de estado OIDC es reutilizado repetidamente por el navegador dentro de la aplicación cuando los usuarios reintentan el inicio de sesión.

Ejemplo (saneado):
• Misma suma de comprobación de estado vista 14 veces
• Todas las solicitudes de:

Snapchat/13.77.0.51 (like Safari…, panda)

• Repartidas en aproximadamente 1 hora de intentos de inicio de sesión repetidos
• Cada intento resulta en:

/auth/oidc/callback → /auth/failure?message=csrf_detected

Esto sugiere fuertemente que:
• el navegador nunca almacena con éxito la cookie de sesión que contiene el estado original
• cada reintento reutiliza el mismo parámetro de estado obsoleto
• por lo tanto, Discourse rechaza correctamente la devolución de llamada cada vez

En contraste, cuando el mismo usuario abre el enlace en Safari, se genera un nuevo estado y el inicio de sesión tiene éxito de inmediato.

Por lo tanto, este parece ser un modo de fallo totalmente determinista en ciertos navegadores integrados de iOS en lugar de una condición de tiempo o de carrera.

Desde la perspectiva de Discourse, la protección CSRF se comporta exactamente como se pretende: el entorno del navegador simplemente no puede mantener la continuidad de la sesión requerida.

Creo que esto apoya aún más que:
• esto no es algo que un componente de tema o JavaScript del cliente pueda mitigar
• y que el único manejo confiable es la mensajería del lado del servidor y la documentación

Publico por si este punto de datos es útil para confirmar el comportamiento esperado.