La connexion OIDC via l'application iOS Discourse échoue occasionnellement avec csrf_detected au rappel

Bonjour,

J’utilise Discourse (2026.2.0-latest (f7cec86997)) avec OpenID Connect (Azure / Entra ID comme IdP).

J’ai remarqué un échec de connexion occasionnel qui ne semble se produire que lorsque les utilisateurs tentent de se connecter via l’application iOS de Discourse.

D’après les journaux du serveur, le flux ressemble à ceci :

POST /auth/oidc
GET  /auth/oidc/callback?...state=...
(oidc) Échec de l'authentification ! csrf_detected

Le rappel atteint bien Discourse, mais la validation CSRF/state échoue, donc aucun compte utilisateur n’est créé.

Les journaux environnants suggèrent que cela se produit dans le flux de transfert d’application :

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

Du point de vue de l’utilisateur, rien d’évident n’apparaît - il est simplement renvoyé à l’écran de connexion et ne se souvient souvent pas avoir vu d’erreur.

Cela ne semble pas se produire lors de la connexion via Safari ou les navigateurs de bureau.

Mon hypothèse est que cela est lié au partitionnement des cookies d’iOS / au changement de contexte entre le navigateur intégré à l’application et le rappel de l’application.

Je voulais juste vérifier :

  • s’il s’agit d’un comportement attendu avec OIDC + l’application iOS
  • et s’il existe des atténuations recommandées au-delà de s’assurer d’une origine canonique HTTPS stricte

Merci - je peux fournir des extraits de journaux anonymisés si cela est utile.

Point de données supplémentaire provenant des journaux d’accès nginx :

Un échec représentatif (2026-01-25 11:44:10 UTC) montre que la requête de rappel OIDC provient d’un UA de navigateur intégré iOS (Snapchat), et non de l’UA de la vue web de l’application iOS Discourse :

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/

Immédiatement suivi par :
GET /auth/failure?message=csrf_detected&strategy=oidc

Il semble donc que le flux OAuth soit parfois initié à l’intérieur d’un navigateur intégré iOS (Snapchat/autre),
puis que le transfert se produise (j’ai également vu des journaux contenant auth_redirect=discourse://auth_redirect),
et que le cookie de session/l’état ne survivent pas de manière cohérente.
Paramètre actuel : SiteSetting.same_site_cookies = "Lax".

Question : le flux d’authentification de l’application mobile de Discourse est-il censé être fiable lorsque la connexion est initiée à partir de navigateurs intégrés iOS qui effectuent ensuite un lien profond vers l’application Discourse ?
Le passage de same_site_cookies à « None » serait-il la mitigation recommandée, ou existe-t-il une meilleure approche ?

Suite à des investigations supplémentaires et à une confirmation d’utilisation réelle.

Après avoir creusé un peu plus, je pense qu’il s’agit bien d’une limitation du navigateur intégré à iOS (WKWebView) plutôt que de quelque chose de spécifique à Discourse ou à la configuration Azure.

Ce que j’ai pu confirmer

D’après les journaux nginx + Rails et les tests utilisateurs :

  • Le flux OAuth est parfois initié à l’intérieur d’un navigateur intégré à iOS (par exemple, Snapchat)
  • La connexion Microsoft (login.microsoftonline.com) est chargée à l’intérieur de ce navigateur intégré
  • Le rappel OIDC atteint Discourse avec succès
  • Mais le cookie de session contenant la valeur d’état ne survit pas
  • Ce qui résulte en un :
GET /auth/failure?message=csrf_detected&strategy=oidc

déterministe.

Cela se produit même si le référent est Microsoft et que l’URI de redirection est correcte.

Un UA représentatif :

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

Donc, bien que l’interface utilisateur “oneboxe” joliment Microsoft, le navigateur sous-jacent reste le WKWebView de Snapchat, et non Safari / ASWebAuthenticationSession.

L’interception par composant de thème ne fonctionne pas

J’ai tenté d’atténuer cela côté client en utilisant un composant de thème :

  • détecter les UA de navigateurs intégrés connus
  • bloquer les liens /auth/oidc
  • afficher une superposition persistante demandant aux utilisateurs d’ouvrir dans Safari/Chrome

Cependant, cela n’intercepte pas le flux de manière fiable, car :

  • la redirection OAuth est initiée côté serveur
  • le cookie d’état doit déjà exister avant que le JS client ne s’exécute
  • au moment où le thème se charge, les dégâts sont déjà faits

Donc, un composant de thème ne peut pas empêcher de manière fiable ce mode d’échec.

Ce qui fonctionne de manière fiable

La seule atténuation que j’ai trouvée qui fonctionne de manière cohérente est de remplacer le texte du site :

login.omniauth_error.csrf_detected

pour expliquer explicitement que :

  • la connexion a échoué à cause d’un navigateur intégré
  • les utilisateurs doivent ouvrir le site dans Safari ou Chrome
  • puis réessayer la connexion

Ce message est rendu côté serveur après l’échec et apparaît donc même dans les contextes de navigateurs intégrés défectueux.

Cela a considérablement réduit la confusion des utilisateurs, car auparavant, les utilisateurs étaient silencieusement renvoyés à la page de connexion et ne réalisaient souvent pas que quelque chose n’allait pas.

À propos des cookies SameSite

Je n’ai pas changé same_site_cookies à “None”.

Étant donné qu’il s’agit d’un problème d’isolation WKWebView (plutôt qu’une navigation inter-sites dans Safari), la modification de SameSite ne semble pas aborder la cause profonde et pourrait introduire des compromis de sécurité inutiles.

Question ouverte

Compte tenu de ce qui précède, je voulais vérifier si :

  • ce comportement est considéré comme attendu lorsque l’OAuth est initié à partir de navigateurs intégrés iOS
  • et si Discourse a l’intention de prendre en charge ce flux, ou simplement de documenter que la connexion doit se faire à partir d’un vrai navigateur

Il pourrait également être utile pour Discourse de faire apparaître un message par défaut plus explicite pour csrf_detected dans les contextes OmniAuth, car ce mode d’échec semble de plus en plus courant avec les utilisateurs étudiants arrivant via des liens Snapchat / Instagram.

Je suis heureux de fournir plus de journaux anonymisés si cela est utile - mais à ce stade, le comportement semble très cohérent et reproductible.

Merci d’avoir examiné cela.

Un point de données supplémentaire qui pourrait aider à confirmer qu’il s’agit d’une limitation déterministe de WKWebView plutôt que d’un comportement intermittent.

En corrélant les journaux d’accès nginx sur plusieurs échecs, j’ai constaté que la même valeur d’état OIDC est réutilisée de manière répétée par le navigateur intégré lorsque les utilisateurs retentent la connexion.

Exemple (assaini) :
• Même hachage d’état vu 14 fois
• Toutes les requêtes proviennent de :

Snapchat/13.77.0.51 (like Safari…, panda)

• Réparties sur environ 1 heure de tentatives de connexion répétées
• Chaque tentative aboutit à :

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

Ceci suggère fortement que :
• le navigateur ne parvient jamais à stocker le cookie de session contenant l’état d’origine
• chaque nouvelle tentative réutilise le même paramètre d’état obsolète
• Discourse rejette donc correctement le rappel à chaque fois

En revanche, lorsque le même utilisateur ouvre le lien dans Safari, un nouvel état est généré et la connexion réussit immédiatement.

Il semble donc s’agir d’un mode d’échec entièrement déterministe dans certains navigateurs intégrés iOS plutôt que d’une condition de temporisation ou de concurrence.

Du point de vue de Discourse, la protection CSRF se comporte exactement comme prévu - l’environnement du navigateur ne peut tout simplement pas maintenir la continuité de session requise.

Je pense que cela confirme davantage que :
• ce n’est pas quelque chose qu’un composant de thème ou du JavaScript client peut atténuer
• et que la seule gestion fiable est la messagerie côté serveur et la documentation

Je publie ceci au cas où ce point de données serait utile pour confirmer le comportement attendu.