Accesso OIDC tramite app iOS di Discourse fallisce occasionalmente con csrf_detected al callback

Ciao,

Sto eseguendo Discourse (2026.2.0-latest (f7cec86997)) con OpenID Connect (Azure / Entra ID come IdP).

Ho notato un fallimento occasionale nell’accesso che sembra verificarsi solo quando gli utenti tentano di accedere tramite l’app Discourse per iOS.

Dai log del server, il flusso appare così:

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

Il callback raggiunge Discourse, ma la validazione CSRF/state fallisce, quindi nessun account utente viene creato.

I log circostanti suggeriscono che ciò stia accadendo nel flusso di passaggio dell’applicazione:

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

Dal punto di vista dell’utente, non appare nulla di ovvio: vengono semplicemente reindirizzati alla schermata di accesso e spesso non ricordano di aver visto un errore.

Questo non sembra verificarsi durante l’accesso tramite Safari o browser desktop.

La mia ipotesi è che ciò sia correlato al partizionamento dei cookie di iOS / al cambio di contesto tra il browser in-app e il callback dell’app.

Volevo solo verificare:

  • se questo è un comportamento previsto con OIDC + l’app iOS
  • e se ci sono mitigazioni consigliate oltre ad assicurare un’origine HTTPS canonica rigorosa

Grazie - sono lieto di fornire frammenti di log anonimizzati se utili.

Dato aggiuntivo dai log di accesso di nginx:

Un errore rappresentativo (2026-01-25 11:44:10 UTC) mostra che la richiesta di callback OIDC proviene da un UA di browser in-app di iOS (Snapchat), non dall’UA della webview dell’app iOS di 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/

Immediatamente seguito da:
GET /auth/failure?message=csrf_detected&strategy=oidc

Quindi sembra che il flusso OAuth venga talvolta avviato all’interno di un browser in-app di iOS (Snapchat/altro),
quindi si verifica il passaggio (ho anche visto log contenenti auth_redirect=discourse://auth_redirect),
e il cookie di sessione/stato non sopravvive in modo coerente.
Impostazione attuale: SiteSetting.same_site_cookies = "Lax".

Domanda: il flusso di autenticazione dell’app mobile di Discourse è previsto che sia affidabile quando l’accesso viene avviato da browser in-app di iOS che quindi reindirizzano all’app Discourse tramite deep-link?
La modifica di same_site_cookies a “None” sarebbe la mitigazione consigliata, o c’è un approccio migliore?

A seguito di ulteriori indagini e conferme derivanti dall’uso nel mondo reale.

Dopo aver approfondito, penso che questo sia effettivamente un limite del browser in-app di iOS (WKWebView) piuttosto che qualcosa di specifico di Discourse o della configurazione di Azure.

Ciò che sono riuscito a confermare

Dai log di nginx + Rails e dai test utente:

  • Il flusso OAuth viene talvolta avviato all’interno di un browser in-app di iOS (ad esempio Snapchat)
  • L’accesso a Microsoft (login.microsoftonline.com) viene caricato all’interno di quel browser in-app
  • Il callback OIDC raggiunge Discourse con successo
  • Ma il cookie di sessione contenente il valore di stato non sopravvive
  • Risultando in un deterministico:
GET /auth/failure?message=csrf_detected&strategy=oidc

Ciò si verifica anche se il referer è Microsoft e l’URI di reindirizzamento è corretto.

Un UA rappresentativo:

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

Quindi, sebbene l’interfaccia utente “onebox” Microsoft in modo elegante, il browser sottostante è ancora il WKWebView di Snapchat, non Safari / ASWebAuthenticationSession.

L’intercettazione del componente tema non funziona

Ho tentato di mitigare questo problema lato client utilizzando un componente tema:

  • rilevando i UA di browser in-app noti
  • bloccando i link /auth/oidc
  • visualizzando un overlay persistente che istruisce gli utenti ad aprire in Safari/Chrome

Tuttavia, questo non intercetta in modo affidabile il flusso, perché:

  • il reindirizzamento OAuth viene avviato lato server
  • il cookie di stato deve esistere prima che il JS client venga eseguito
  • quando il tema viene caricato, il danno è già fatto

Quindi un componente tema non può prevenire in modo affidabile questa modalità di errore.

Ciò che funziona in modo affidabile

L’unica mitigazione che ho trovato che funziona in modo coerente è sovrascrivere il testo del sito:

login.omniauth_error.csrf_detected

per spiegare esplicitamente che:

  • l’accesso è fallito a causa di un browser in-app
  • gli utenti dovrebbero aprire il sito in Safari o Chrome
  • quindi riprovare l’accesso

Questo messaggio viene renderizzato lato server dopo il fallimento e quindi appare anche in contesti di browser in-app non funzionanti.

Ciò ha ridotto significativamente la confusione degli utenti, poiché in precedenza gli utenti venivano riportati silenziosamente alla pagina di accesso e spesso non si rendevano conto che qualcosa fosse andato storto.

Informazioni sui cookie SameSite

Non ho impostato same_site_cookies su “None”.

Dato che si tratta di un problema di isolamento di WKWebView (piuttosto che di navigazione cross-site in Safari), la modifica di SameSite non sembra affrontare la causa principale e potrebbe introdurre compromessi di sicurezza non necessari.

Domanda aperta

Dato quanto sopra, volevo verificare se:

  • questo comportamento è considerato previsto quando OAuth viene avviato da browser in-app iOS
  • e se Discourse intende supportare tale flusso, o semplicemente documentare che l’accesso deve avvenire da un browser reale

Potrebbe anche essere utile per Discourse mostrare un messaggio predefinito più esplicito per csrf_detected nei contesti OmniAuth, poiché questa modalità di errore sembra essere sempre più comune con gli utenti studenti che arrivano tramite link di Snapchat / Instagram.

Sono lieto di fornire altri log anonimizzati se utili, ma a questo punto il comportamento sembra molto coerente e riproducibile.

Grazie per aver dato un’occhiata.

Un ulteriore dato che potrebbe aiutare a confermare che si tratta di una limitazione deterministica di WKWebView piuttosto che di un comportamento intermittente.

Correlazionando i log di accesso di nginx su più fallimenti, ho scoperto che lo stesso valore di stato OIDC viene riutilizzato ripetutamente dal browser in-app quando gli utenti tentano nuovamente l’accesso.

Esempio (sanificato):
• Stesso hash di stato visto 14 volte
• Tutte le richieste da:

Snapchat/13.77.0.51 (like Safari…, panda)

• Distribuite su circa 1 ora di tentativi di accesso ripetuti
• Ogni tentativo si traduce in:

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

Ciò suggerisce fortemente che:
• il browser non memorizza mai con successo il cookie di sessione contenente lo stato originale
• ogni tentativo di ripetizione riutilizza lo stesso parametro di stato obsoleto
• Discourse sta quindi rifiutando correttamente la callback ogni volta

Al contrario, quando lo stesso utente apre il link in Safari, viene generato un nuovo stato e l’accesso riesce immediatamente.

Quindi questo sembra essere una modalità di fallimento completamente deterministica in alcuni browser in-app di iOS piuttosto che una condizione di temporizzazione o di gara.

Dal punto di vista di Discourse, la protezione CSRF si sta comportando esattamente come previsto: l’ambiente del browser semplicemente non è in grado di mantenere la continuità di sessione richiesta.

Penso che questo supporti ulteriormente che:
• non è qualcosa che un componente tema o JavaScript client può mitigare
• e che l’unica gestione affidabile è la messaggistica lato server e la documentazione

Pubblico nel caso in cui questo dato sia utile per confermare il comportamento previsto.