Discourse iOSアプリ経由のOIDCログインが、コールバック時にcsrf_detectedで時々失敗する

こんにちは。

OpenID Connect(IdPとしてAzure / Entra ID)を使用してDiscourse(2026.2.0-latest (f7cec86997))を実行しています。

時折、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コールバックリクエストがDiscourse iOSアプリのWebview UAではなく、iOSのアプリ内ブラウザUA(Snapchat)から来ていることを示しています。

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」に変更することが推奨される緩和策でしょうか、それともより良いアプローチがありますか?

さらなる調査と実使用による確認を追記します。

さらに掘り下げた結果、これはDiscourseやAzureの設定に特有のものではなく、iOSのアプリ内ブラウザ(WKWebView)の制限である可能性が高いと考えられます。

確認できたこと

nginx + Railsのログとユーザーテストから、以下の点が確認できました。

  • OAuthフローがiOSのアプリ内ブラウザ(例:Snapchat)内で開始されることがある
  • Microsoftのログイン(login.microsoftonline.com)がそのアプリ内ブラウザ内で読み込まれる
  • OIDCコールバックはDiscourseに正常に到達する
  • しかし、state値を含むセッションクッキーが保持されない
  • 結果として、決定論的に以下が発生する
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)

したがって、UIはMicrosoftをきれいに「ワンボックス」しますが、基盤となるブラウザはSafariやASWebAuthenticationSessionではなく、SnapchatのWKWebViewのままです。

テーマコンポーネントによるインターセプトは機能しない

クライアント側でテーマコンポーネントを使用してこれを軽減しようと試みました。

  • 既知のアプリ内ブラウザUAを検出する
  • /auth/oidc リンクをブロックする
  • Safari/Chromeで開くよう指示する永続的なオーバーレイを表示する

しかし、これはフローを確実にインターセプトできません。なぜなら:

  • OAuthリダイレクトはサーバー側で開始される
  • クライアントJSが実行される前にstateクッキーが存在している必要がある
  • テーマが読み込まれる頃には、すでに手遅れになっている

そのため、テーマコンポーネントではこの障害モードを確実に防ぐことはできません。

確実に機能すること

一貫して機能する唯一の軽減策は、サイトテキストを上書きすることです。

login.omniauth_error.csrf_detected

これに以下を明示的に説明するメッセージを設定します。

  • ログインがアプリ内ブラウザのために失敗したこと
  • ユーザーはサイトをSafariまたはChromeで開く必要があること
  • その後、ログインを再試行する必要があること

このメッセージは失敗後にサーバー側でレンダリングされるため、壊れたアプリ内ブラウザの状況下でも表示されます。

これにより、以前はユーザーがサイレントにログインページに戻され、何が起こったのか気づかないことが多かったため、ユーザーの混乱が大幅に減少しました。

SameSiteクッキーについて

same_site_cookiesを「None」に変更していません。

これがWKWebViewの分離の問題(Safariでのクロスサイトナビゲーションではない)であるため、SameSiteを変更しても根本原因に対処するようには見えず、不必要なセキュリティ上のトレードオフをもたらす可能性があります。

未解決の質問

上記を踏まえ、以下の点について確認したいです。

  • この動作は、OAuthがiOSのアプリ内ブラウザから開始された場合に予期されるものとして扱われるのか
  • Discourseは、そのフローをサポートする意図があるのか、あるいはログインは実際のブラウザから行う必要があると単に文書化するのか

また、この障害モードはSnapchat/Instagramのリンク経由でアクセスする学生ユーザーの間でますます一般的になっているようなので、DiscourseがOmniAuthコンテキストに対してより明示的なデフォルトメッセージを表面化させると役立つかもしれません。

もし役立つようであれば、さらに匿名化されたログを提供できますが、現時点では動作は非常に一貫しており再現可能です。

ご確認いただきありがとうございます。

これを決定論的なWKWebViewの制限として確認するのに役立つ可能性のある、もう1つのデータポイントがあります。

複数の失敗にわたる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保護は意図したとおりに機能しており、ブラウザ環境が要求されるセッションの継続性を維持できないだけです。

これにより、次のことがさらに裏付けられると考えられます。
• これはテーマコンポーネントやクライアントJSでは軽減できない
• そして、唯一信頼できる対処法はサーバーサイドのメッセージングとドキュメントである

このデータポイントが、期待される動作を確認するのに役立つ場合に備えて投稿します。