OIDC-Anmeldung über die Discourse iOS App schlägt gelegentlich mit csrf_detected beim Callback fehl

Hallo,

ich betreibe Discourse (2026.2.0-latest (f7cec86997)) mit OpenID Connect (Azure / Entra ID als IdP).

Mir ist ein gelegentliches Anmeldeversagen aufgefallen, das anscheinend nur auftritt, wenn Benutzer versuchen, sich über die Discourse iOS App anzumelden.

Aus den Serverprotokollen sieht der Ablauf wie folgt aus:

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

Der Callback erreicht Discourse zwar, aber die CSRF-/State-Validierung schlägt fehl, sodass kein Benutzerkonto erstellt wird.

Die umgebenden Protokolle deuten darauf hin, dass dies im App-Handoff-Fluss geschieht:

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

Aus Benutzersicht erscheint nichts Offensichtliches – sie werden einfach zum Anmeldebildschirm zurückgeleitet und erinnern sich oft nicht daran, eine Fehlermeldung gesehen zu haben.

Dies scheint bei der Anmeldung über Safari oder Desktop-Browser nicht aufzutreten.

Ich gehe davon aus, dass dies mit der iOS-Cookie-Partitionierung / dem Kontextwechsel zwischen dem In-App-Browser und dem App-Callback zusammenhängt.

Ich wollte nur kurz überprüfen:

  • ob dies das erwartete Verhalten bei OIDC + der iOS App ist
  • und ob es empfohlene Abhilfemaßnahmen gibt, abgesehen davon, sicherzustellen, dass eine strikte kanonische HTTPS-Origin verwendet wird.

Danke – ich kann gerne anonymisierte Protokollausschnitte zur Verfügung stellen, falls dies hilfreich ist.

Zusätzlicher Datenpunkt aus den nginx-Zugriffsprotokollen:

Ein repräsentativer Fehler (2026-01-25 11:44:10 UTC) zeigt, dass die OIDC-Callback-Anfrage von einem iOS In-App-Browser UA (Snapchat) kommt und nicht vom Discourse iOS App Webview UA:

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/

Unmittelbar gefolgt von:
GET /auth/failure?message=csrf_detected&strategy=oidc

Es sieht also so aus, als ob der OAuth-Fluss manchmal innerhalb eines iOS In-App-Browsers (Snapchat/andere) initiiert wird,
dann der Übergang stattfindet (ich habe auch Protokolle mit auth_redirect=discourse://auth_redirect gesehen),
und der Sitzungscookie/Status nicht konsistent überlebt.
Aktuelle Einstellung: SiteSetting.same_site_cookies = "Lax".

Frage: Ist der mobile App-Authentifizierungsfluss von Discourse zuverlässig, wenn die Anmeldung von iOS In-App-Browsern initiiert wird, die dann einen Deep-Link zur Discourse-App herstellen?
Wäre das Wechseln von same_site_cookies auf „None“ die empfohlene Abhilfe, oder gibt es einen besseren Ansatz?

Nach weiteren Nachforschungen und Bestätigung durch den realen Einsatz.

Nach eingehender Prüfung glaube ich, dass dies tatsächlich eine Einschränkung des iOS In-App-Browsers (WKWebView) und nicht etwas Spezifisches für Discourse oder die Azure-Konfiguration ist.

Was ich bestätigen konnte

Anhand von nginx- und Rails-Protokollen sowie Benutzertests:

  • Der OAuth-Fluss wird manchmal innerhalb eines iOS In-App-Browsers (z. B. Snapchat) initiiert
  • Die Microsoft-Anmeldung (login.microsoftonline.com) wird in diesem In-App-Browser geladen
  • Der OIDC-Rückruf erreicht Discourse erfolgreich
  • Aber das Sitzungscookie, das den Zustands-Wert enthält, überlebt nicht
  • Was zu einem deterministischen Ergebnis führt:
GET /auth/failure?message=csrf_detected&strategy=oidc

Dies geschieht, obwohl der Referer Microsoft ist und die Weiterleitungs-URI korrekt ist.

Ein repräsentativer UA:

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

Obwohl die Benutzeroberfläche Microsoft schön „einbettet“ (oneboxes), ist der zugrunde liegende Browser immer noch der WKWebView von Snapchat und nicht Safari / ASWebAuthenticationSession.

Theming-Komponenten-Abfangen funktioniert nicht

Ich habe versucht, dies clientseitig mithilfe einer Theming-Komponente zu mildern:

  • Erkennen bekannter In-App-Browser-UAs
  • Blockieren von /auth/oidc-Links
  • Anzeigen eines persistenten Overlays, das Benutzer anweist, in Safari/Chrome zu öffnen

Dies fängt den Fluss jedoch nicht zuverlässig ab, weil:

  • Die OAuth-Weiterleitung serverseitig initiiert wird
  • Das Zustands-Cookie bereits existieren muss, bevor Client-JS ausgeführt wird
  • Bis das Theme geladen wird, ist der Schaden bereits angerichtet

Eine Theming-Komponente kann dieses Fehlerverhalten also nicht zuverlässig verhindern.

Was zuverlässig funktioniert

Die einzige Abhilfemaßnahme, die ich gefunden habe und die konsistent funktioniert, ist das Überschreiben des Website-Textes:

login.omniauth_error.csrf_detected

um explizit zu erklären, dass:

  • die Anmeldung aufgrund eines In-App-Browsers fehlgeschlagen ist
  • Benutzer die Website in Safari oder Chrome öffnen sollten
  • und dann die Anmeldung erneut versuchen sollten

Diese Nachricht wird serverseitig nach dem Fehler gerendert und erscheint daher auch in fehlerhaften In-App-Browser-Kontexten.

Dies hat die Verwirrung der Benutzer erheblich reduziert, da die Benutzer zuvor stillschweigend zur Anmeldeseite zurückgeleitet wurden und oft nicht merkten, dass etwas schiefgelaufen war.

Über SameSite-Cookies

Ich habe same_site_cookies nicht auf „None“ umgestellt.

Da es sich hier um ein WKWebView-Isolationsproblem handelt (und nicht um eine seitenübergreifende Navigation in Safari), scheint die Änderung von SameSite die Grundursache nicht zu beheben und könnte unnötige Sicherheitskompromisse mit sich bringen.

Offene Frage

Angesichts dessen wollte ich überprüfen, ob:

  • dieses Verhalten als erwartet gilt, wenn OAuth von iOS In-App-Browsern initiiert wird
  • und ob Discourse beabsichtigt, diesen Fluss zu unterstützen, oder ob es einfach dokumentieren soll, dass die Anmeldung von einem echten Browser aus erfolgen muss

Es könnte auch für Discourse nützlich sein, eine explizitere Standardnachricht für csrf_detected in OmniAuth-Kontexten anzuzeigen, da dieses Fehlerverhalten bei Studenten, die über Snapchat-/Instagram-Links kommen, zunehmend häufiger auftritt.

Gerne stelle ich weitere anonymisierte Protokolle zur Verfügung, falls dies hilfreich ist – aber an diesem Punkt scheint das Verhalten sehr konsistent und reproduzierbar zu sein.

Vielen Dank für die Prüfung.

Ein zusätzlicher Datenpunkt, der dies bestätigen könnte, ist eine deterministische WKWebView-Einschränkung und kein intermittierendes Verhalten.

Durch die Korrelation von Nginx-Zugriffsprotokollen über mehrere Fehler hinweg habe ich festgestellt, dass derselbe OIDC-Zustandswert wiederholt vom In-App-Browser verwendet wird, wenn Benutzer den Anmeldevorgang wiederholen.

Beispiel (bereinigt):
• Derselbe Zustands-Hash wurde 14 Mal gesehen
• Alle Anfragen von:

Snapchat/13.77.0.51 (like Safari…, panda)

• Verteilt über ca. 1 Stunde wiederholter Anmeldeversuche
• Jeder Versuch führt zu:

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

Dies deutet stark darauf hin, dass:
• der Browser das Sitzungscookie, das den ursprünglichen Zustand enthält, niemals erfolgreich speichert
• jeder erneute Versuch denselben veralteten Zustabsparameter wiederverwendet
• Discourse daher den Callback jedes Mal korrekt ablehnt

Im Gegensatz dazu wird bei demselben Benutzer, der den Link in Safari öffnet, ein neuer Zustand generiert und die Anmeldung sofort erfolgreich ist.

Es scheint sich also um einen vollständig deterministischen Fehlerfall in bestimmten iOS-In-App-Browsern und nicht um eine Timing- oder Race Condition zu handeln.

Aus Sicht von Discourse verhält sich der CSRF-Schutz genau wie vorgesehen – die Browserumgebung kann die erforderliche Sitzungskontinuität einfach nicht aufrechterhalten.

Ich denke, dies unterstützt weiter, dass:
• dies nichts ist, was eine Theme-Komponente oder Client-JS mildern kann
• und dass die einzig zuverlässige Handhabung serverseitige Nachrichten und Dokumentation ist

Ich poste dies für den Fall, dass dieser Datenpunkt nützlich ist, um das erwartete Verhalten zu bestätigen.