Discourse iOS 应用通过 OIDC 登录在回调时偶尔会因 csrf_detected 而失败

您好,

我正在运行 Discourse (2026.2.0-latest (f7cec86997)) 并使用 OpenID Connect(Azure / Entra ID 作为 IdP)。

我注意到偶尔会出现登录失败的情况,但这似乎只发生在用户尝试通过 Discourse iOS 应用程序登录时。

从服务器日志来看,流程如下:

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

回调确实到达了 Discourse,但 CSRF/state 验证失败,因此没有创建用户帐户。

周围的日志表明这发生在应用程序交接流程中:

  • application_name=Discourse - iPhone
  • auth_redirect=discourse://auth_redirect

从用户的角度来看,没有明显的迹象——他们只是被返回到登录屏幕,而且通常不记得看到过错误。

通过 Safari 或桌面浏览器登录时似乎不会发生这种情况。

我的假设是这与 iOS 的 Cookie 分区/应用内浏览器和应用回调之间的上下文切换有关。

我只是想确认一下:

  • 这是 OIDC + iOS 应用程序的预期行为吗?
  • 除了确保严格的规范 HTTPS 源之外,还有没有推荐的缓解措施?

谢谢——如果需要,我很乐意提供匿名化的日志片段。

来自 nginx 访问日志的额外数据点:

一个有代表性的失败(2026-01-25 11:44:10 UTC)显示 OIDC 回调请求来自 iOS 应用内浏览器的用户代理(Snapchat),而不是 Discourse iOS 应用的 webview 用户代理:

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/

紧接着是:
GET /auth/failure?message=csrf_detected&strategy=oidc

因此,看起来 OAuth 流程有时会在 iOS 应用内浏览器(Snapchat/其他)中启动,
然后发生跳转(我也在日志中看到包含 auth_redirect=discourse://auth_redirect 的记录),
并且会话 cookie/状态不能持续保持一致。
当前设置:SiteSetting.same_site_cookies = "Lax"

问题:当登录从 iOS 应用内浏览器启动并随后深度链接到 Discourse 应用时,Discourse 移动应用的身份验证流程是否可以保证可靠?
same_site_cookies 切换到“None”是推荐的缓解措施,还是有更好的方法?

在进行一些额外的调查并从实际使用中确认后,我有了以下发现。

深入研究后,我认为这确实是 iOS 应用内浏览器 (WKWebView) 的限制,而不是 Discourse 或 Azure 配置的特定问题。

我确认了以下几点

根据 nginx + Rails 日志和用户测试:

  • OAuth 流程有时在 iOS 应用内浏览器(例如 Snapchat)中启动
  • Microsoft 登录 (login.microsoftonline.com) 在该应用内浏览器中加载
  • OIDC 回调成功到达 Discourse
  • 但包含状态值的会话 cookie 无法保留
  • 导致确定性的结果:
GET /auth/failure?message=csrf_detected&strategy=oidc

即使引用者是 Microsoft 且重定向 URI 正确,也会发生这种情况。

一个代表性的用户代理 (UA):

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

因此,尽管用户界面很好地“一框显示”了 Microsoft,但底层浏览器仍然是 Snapchat 的 WKWebView,而不是 Safari / ASWebAuthenticationSession。

主题组件拦截不起作用

我尝试使用主题组件在客户端缓解此问题:

  • 检测已知的应用内浏览器 UA
  • 阻止 /auth/oidc 链接
  • 显示持久性覆盖层,指示用户在 Safari/Chrome 中打开

然而,这无法可靠地拦截该流程,因为:

  • OAuth 重定向是服务器端启动的
  • 状态 cookie 必须在客户端 JavaScript 运行之前就已存在
  • 当主题加载时,损害已经造成

因此,主题组件无法可靠地防止这种失败模式。

哪些有效且可靠

我发现唯一一致有效的方法是覆盖站点文本:

login.omniauth_error.csrf_detected

明确解释:

  • 登录因应用内浏览器而失败
  • 用户应在 Safari 或 Chrome 中打开网站
  • 然后重试登录

此消息在失败后由服务器端渲染,因此即使在出现问题的应用内浏览器环境中也会显示。

这大大减少了用户的困惑,因为以前用户会被静默地返回登录页面,而且通常没有意识到出了什么问题。

关于 SameSite cookie

我没有将 same_site_cookies 更改为“None”。

鉴于这是一个 WKWebView 隔离问题(而不是 Safari 中的跨站导航),更改 SameSite 似乎无法解决根本原因,并可能带来不必要的安全权衡。

待解决的问题

鉴于以上情况,我想确认一下:

  • 当 OAuth 从 iOS 应用内浏览器启动时,这种行为是否被认为是预期的
  • Discourse 是否打算支持该流程,或者只是记录登录必须从真正的浏览器中进行

对于 Discourse 来说,可能还有用的是在 OmniAuth 上下文中显示一个更明确的默认消息,因为随着越来越多的学生用户通过 Snapchat / Instagram 链接访问,这种失败模式似乎越来越常见。

如果需要,我很乐意提供更多匿名日志——但此时行为似乎非常一致且可重现。

感谢您的关注。

另一个可能有助于确认这是 WKWebView 的确定性限制而非间歇性行为的数据点是:

通过关联多个失败案例中的 nginx 访问日志,我发现应用内浏览器在用户重试登录时会重复使用相同的 OIDC 状态值。

示例(已清理):
• 观察到相同的状态哈希 14 次
• 所有请求来自:

Snapchat/13.77.0.51 (like Safari…, panda)

• 分布在大约 1 小时的重复登录尝试中
• 每次尝试的结果是:

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

这有力地表明:
• 浏览器从未成功存储包含原始状态的会话 cookie
• 每次重试都会重用相同的陈旧状态参数
• 因此,Discourse 每次都会正确拒绝回调

相比之下,当同一用户在 Safari 中打开链接时,会生成一个新状态,登录会立即成功。

因此,这似乎是某些 iOS 应用内浏览器中完全确定的失败模式,而不是时序或竞态条件。

从 Discourse 的角度来看,CSRF 保护的行为完全符合预期——浏览器环境根本无法维持所需的会话连续性。

我认为这进一步支持了:
• 这不是主题组件或客户端 JavaScript 可以缓解的问题
• 唯一可靠的处理方式是服务器端消息传递和文档记录

发帖以防此数据点有助于确认预期行为。