OIDC csrf_detected pode mascarar cancelamento do usuário / rejeição de consentimento - documentação poderia esclarecer inspeção de logs

Continuando a discussão de OIDC login via Discourse iOS app occasionally fails with csrf_detected on callback:

Olá a todos,

Esta é uma observação de acompanhamento ligada ao meu tópico anterior sobre falhas de login OIDC iniciadas a partir de navegadores internos do iOS. Essa discussão focou em falhas determinísticas de csrf_detected causadas pelo isolamento de cookies do WKWebView, que agora parecem bem compreendidas e esperadas.

Este tópico vinculado é sobre clareza para os operadores, não um bug.


Observação

Ao investigar uma série de falhas de login OIDC, notei que a mesma mensagem de erro na superfície do Discourse:

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

pode corresponder a múltiplas causas raiz fundamentalmente diferentes, dependendo do que o IdP retornou.

Apenas a partir da interface do usuário do aplicativo, esses casos são indistinguíveis. A diferença só é visível ao inspecionar Admin → Logs → env / params.


Exemplos vistos na prática (Azure / Entra ID)

Além da perda de cookies do navegador interno, observei callbacks onde o Entra ID retorna explicitamente erros estruturados como:

Usuário recusou o consentimento

error=consent_required
error_description=AADSTS65004: User declined to consent to access the app

Usuário cancelou o login

error=access_denied
error_subcode=cancel

Em ambos os casos:
\t•\tO Azure identificou o usuário com sucesso
\t•\tO usuário escolheu explicitamente não prosseguir (recusar / cancelar)
\t•\tO Discourse recebe o callback
\t•\tO fluxo acaba resolvendo para /auth/failure?message=csrf_detected

Do ponto de vista do Discourse, este é um comportamento correto e seguro - o estado não pode ser validado ou concluído - mas a razão subjacente é muito diferente de um cookie de sessão ausente.


Por que isso é importante para os operadores

Sem verificar o env/params do log, um administrador que vê falhas repetidas de csrf_detected pode razoavelmente presumir:
\t•\tcookies quebrados
\t•\tconfiguração incorreta do SameSite
\t•\tproblemas no navegador móvel
\t•\tinstabilidade do IdP

…quando, na realidade, algumas dessas falhas são simplesmente usuários escolhendo não consentir ou cancelar o prompt da Microsoft.

Essa distinção só fica clara se você já souber inspecionar a carga de log bruta.


Sugestão (apenas documentação / UX)

Não estou sugerindo nenhuma mudança de comportamento no OmniAuth ou no tratamento de CSRF.

Poderia ser útil se a documentação ou o guia de solução de problemas notassem explicitamente que:
\t•\tcsrf_detected pode ser o erro final para múltiplos resultados de IdP upstream
\t•\tincluindo ações explícitas do usuário, como cancelar ou rejeitar o consentimento
\t•\te que os administradores devem inspecionar Admin → Logs → env / params para distinguir esses casos

Isso facilitaria para os operadores:
\t•\tdiagnosticar corretamente as falhas de login
\t•\tevitar alterações de configuração desnecessárias
\t•\te fornecer orientação precisa aos usuários (“você cancelou / recusou o consentimento” vs “seu navegador bloqueou cookies”).


Contexto

Para maior clareza: isso é separado do problema confirmado do navegador interno do iOS discutido no tópico vinculado. Nesse caso, o IdP nunca chega ao ponto de consentimento do usuário, enquanto aqui o IdP relata explicitamente a intenção do usuário.

Ambos acabam parecendo semelhantes no nível da interface do usuário, a menos que os logs sejam examinados.


Obrigado por ler - estou postando isso principalmente como um ponto de clareza/dados de documentação para outros que executam OIDC em ambientes com muitos estudantes, onde esses casos ocorrem com frequência.

Fico feliz em fornecer exemplos anonimizados, se for útil.

Pequeno detalhe do visualizador de logs (FYI)

Uma pequena coisa que me confundiu ao inspecionar os logs:

Em Admin → Logs → env → params, os valores podem aparecer com sequências de escape como:

\u0026

Isso é apenas escape JSON/Ruby para &. Por exemplo:

error=access_denied\u0026error_subcode=cancel

deve ser lido como:

error=access_denied&error_subcode=cancel

Nada está sendo corrompido ou perdido - é puramente um detalhe de exibição/serialização - mas vale a pena saber para que os operadores não percam o sinal subjacente do IdP ao diagnosticar falhas de csrf_detected.