Login OIDC via app Discourse iOS falha ocasionalmente com csrf_detected no callback

Olá,

Estou executando o Discourse (2026.2.0-latest (f7cec86997)) com OpenID Connect (Azure / Entra ID como IdP).

Notei uma falha ocasional no login que parece ocorrer apenas quando os usuários tentam entrar pelo aplicativo Discourse para iOS.

Nos logs do servidor, o fluxo se parece com:

POST /auth/oidc
GET  /auth/oidc/callback?...state=...
(oidc) Falha de autenticação! csrf_detected

O callback chega ao Discourse, mas a validação CSRF/state falha, então nenhuma conta de usuário é criada.

Os logs ao redor sugerem que isso está acontecendo no fluxo de transferência do aplicativo:

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

Do ponto de vista do usuário, nada óbvio aparece - eles são simplesmente retornados para a tela de login e muitas vezes não se lembram de ter visto um erro.

Isso não parece ocorrer ao fazer login via Safari ou navegadores de desktop.

Minha suposição é que isso está relacionado ao particionamento de cookies do iOS / troca de contexto entre o navegador no aplicativo e o callback do aplicativo.

Eu só queria verificar:

  • se este é o comportamento esperado com OIDC + o aplicativo iOS
  • e se há alguma mitigação recomendada além de garantir uma origem HTTPS canônica estrita

Obrigado - fico feliz em fornecer trechos de log anonimizados, se for útil.

Ponto de dados extra dos logs de acesso do nginx:

Uma falha representativa (2026-01-25 11:44:10 UTC) mostra que a solicitação de retorno de chamada OIDC está vindo de um UA de navegador in-app do iOS (Snapchat), e não do UA do webview do aplicativo Discourse para iOS:

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/

Imediatamente seguido por:
GET /auth/failure?message=csrf_detected&strategy=oidc

Portanto, parece que o fluxo OAuth é iniciado às vezes dentro de um navegador in-app do iOS (Snapchat/outro),
então a transferência ocorre (também vi logs contendo auth_redirect=discourse://auth_redirect),
e o cookie de sessão/estado não sobrevive de forma consistente.
Configuração atual: SiteSetting.same_site_cookies = "Lax".

Pergunta: o fluxo de autenticação do aplicativo móvel do Discourse deve ser confiável quando o login é iniciado a partir de navegadores in-app do iOS que, em seguida, fazem deep-link para o aplicativo Discourse?
Mudar same_site_cookies para “None” seria a mitigação recomendada aqui, ou existe uma abordagem melhor?

Seguindo com alguma investigação adicional e confirmação de uso no mundo real.

Após investigar mais a fundo, acho que esta é, de fato, uma limitação do navegador in-app do iOS (WKWebView) em vez de algo específico do Discourse ou da configuração do Azure.

O que consegui confirmar

A partir dos logs do nginx + Rails e testes com usuários:

  • O fluxo OAuth é iniciado às vezes dentro de um navegador in-app do iOS (ex: Snapchat)
  • O login da Microsoft (login.microsoftonline.com) é carregado dentro desse navegador in-app
  • O callback OIDC alcança o Discourse com sucesso
  • Mas o cookie de sessão contendo o valor do estado não sobrevive
  • Resultando em um determinístico:
GET /auth/failure?message=csrf_detected&strategy=oidc

Isso ocorre mesmo que o referenciador seja a Microsoft e a URI de redirecionamento esteja correta.

Um UA representativo:

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

Portanto, embora a interface do usuário “oneboxe” a Microsoft de forma elegante, o navegador subjacente ainda é o WKWebView do Snapchat, e não o Safari / ASWebAuthenticationSession.

A interceptação do componente de tema não funciona

Tentei mitigar isso no lado do cliente usando um componente de tema:

  • detectando UAs de navegadores in-app conhecidos
  • bloqueando links /auth/oidc
  • exibindo uma sobreposição persistente instruindo os usuários a abrir no Safari/Chrome

No entanto, isso não intercepta o fluxo de forma confiável, porque:

  • o redirecionamento OAuth é iniciado no lado do servidor
  • o cookie de estado deve existir antes que o JS do cliente seja executado
  • quando o tema carrega, o dano já está feito

Portanto, um componente de tema não pode impedir confiavelmente esse modo de falha.

O que funciona de forma confiável

A única mitigação que encontrei que funciona consistentemente é substituir o texto do site:

login.omniauth_error.csrf_detected

para explicar explicitamente que:

  • o login falhou devido a um navegador in-app
  • os usuários devem abrir o site no Safari ou Chrome
  • e então tentar o login novamente

Esta mensagem é renderizada no lado do servidor após a falha e, portanto, aparece mesmo em contextos de navegador in-app quebrados.

Isso reduziu significativamente a confusão do usuário, já que anteriormente os usuários eram silenciosamente retornados à página de login e muitas vezes não percebiam que algo havia dado errado.

Sobre cookies SameSite

Eu não mudei same_site_cookies para “None”.

Dado que este é um problema de isolamento do WKWebView (em vez de navegação entre sites no Safari), mudar SameSite não parece resolver a causa raiz e pode introduzir trocas de segurança desnecessárias.

Pergunta em aberto

Dado o exposto, eu queria verificar se:

  • este comportamento é considerado esperado quando o OAuth é iniciado a partir de navegadores in-app do iOS
  • e se o Discourse pretende suportar esse fluxo, ou simplesmente documentar que o login deve ocorrer a partir de um navegador real

Também pode ser útil para o Discourse exibir uma mensagem padrão mais explícita para csrf_detected em contextos OmniAuth, já que esse modo de falha parece estar se tornando cada vez mais comum com usuários estudantes que chegam por meio de links do Snapchat / Instagram.

Fico feliz em fornecer mais logs anonimizados, se for útil - mas neste ponto o comportamento parece muito consistente e reprodutível.

Obrigado por dar uma olhada.

Um ponto de dados adicional que pode ajudar a confirmar que esta é uma limitação determinística do WKWebView em vez de um comportamento intermitente.

Ao correlacionar os logs de acesso do nginx em múltiplas falhas, descobri que o mesmo valor de estado OIDC é reutilizado repetidamente pelo navegador no aplicativo quando os usuários tentam fazer login novamente.

Exemplo (sanitizado):
• O mesmo hash de estado foi visto 14 vezes
• Todas as requisições de:

Snapchat/13.77.0.51 (like Safari…, panda)

• Espalhadas por ~1 hora de tentativas de login repetidas
• Cada tentativa resulta em:

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

Isso sugere fortemente que:
• o navegador nunca armazena com sucesso o cookie de sessão contendo o estado original
• cada nova tentativa reutiliza o mesmo parâmetro de estado obsoleto
• o Discourse, portanto, rejeita corretamente o callback todas as vezes

Em contraste, quando o mesmo usuário abre o link no Safari, um novo estado é gerado e o login é bem-sucedido imediatamente.

Portanto, este parece ser um modo de falha totalmente determinístico em certos navegadores no aplicativo do iOS, em vez de uma condição de tempo ou corrida.

Do ponto de vista do Discourse, a proteção CSRF está se comportando exatamente como pretendido - o ambiente do navegador simplesmente não consegue manter a continuidade da sessão necessária.

Acho que isso apoia ainda mais que:
• isso não é algo que um componente de tema ou JavaScript do cliente possa mitigar
• e que o único tratamento confiável é a comunicação do lado do servidor e a documentação

Publicando caso este ponto de dados seja útil para confirmar o comportamento esperado.