Вход через OIDC в приложении Discourse для iOS иногда не удаётся с ошибкой csrf_detected при обратном вызове

Здравствуйте,

Я использую Discourse (2026.2.0-latest (f7cec86997) с OpenID Connect (Azure / Entra ID в качестве IdP).

Я заметил периодические сбои входа, которые, похоже, возникают только при попытке пользователей войти через приложение Discourse для iOS.

Судя по логам сервера, процесс выглядит следующим образом:

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

Обратный вызов достигает сервера Discourse, но проверка CSRF/состояния не проходит, поэтому учётная запись пользователя не создаётся.

Смежные логи указывают на то, что это происходит в процессе передачи управления приложением:
application_name=Discourse - iPhone
auth_redirect=discourse://auth_redirect

Со стороны пользователя ничего очевидного не происходит — их просто возвращают на экран входа, и они часто даже не помнят, что видели ошибку.

Это не происходит при входе через Safari или десктопные браузеры.

Я предполагаю, что это связано с разделением куки в iOS или переключением контекста между встроенным браузером и обратным вызовом приложения.

Хотел просто убедиться:
• является ли это ожидаемым поведением при использовании OIDC + приложения для iOS
• и есть ли какие-либо рекомендуемые меры по смягчению проблемы помимо обеспечения строгого канонического HTTPS-источника

Спасибо — готов предоставить анонимизированные фрагменты логов, если это поможет.

Дополнительная точка данных из логов доступа nginx:

Типичный сбой (2026-01-25 11:44:10 UTC) показывает, что запрос обратного вызова OIDC поступает от мобильного браузера внутри приложения iOS (Snapchat), а не от веб-вью приложения Discourse для 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/

Непосредственно за этим следует:
GET /auth/failure?message=csrf_detected&strategy=oidc

Похоже, что OAuth-поток иногда инициируется внутри мобильного браузера внутри приложения iOS (Snapchat или другого),
затем происходит передача (я также видел логи, содержащие auth_redirect=discourse://auth_redirect),
и куки сессии/состояние не сохраняются стабильно.

Текущая настройка: SiteSetting.same_site_cookies = "Lax".

Вопрос: ожидается ли, что поток аутентификации мобильного приложения Discourse будет надежным, когда вход инициируется из мобильных браузеров внутри приложений iOS, которые затем используют глубокие ссылки для перехода в приложение Discourse?
Является ли переключение same_site_cookies на значение “None” рекомендуемым решением в данном случае, или существует более подходящий подход?

Продолжаю с дополнительным расследованием и подтверждением на основе реального использования.

После более глубокого анализа я считаю, что это действительно ограничение встроенного браузера iOS (WKWebView), а не что-то специфичное для конфигурации Discourse или Azure.

Что мне удалось подтвердить

На основе логов nginx + Rails и тестирования пользователями:
• Поток OAuth иногда инициируется внутри встроенного браузера iOS (например, Snapchat)
• Вход в Microsoft (login.microsoftonline.com) загружается внутри этого встроенного браузера
• OIDC-колбэк успешно достигает Discourse
• Но куки сессии, содержащая значение state, не сохраняется
• В результате возникает детерминированная ошибка:

GET /auth/failure?message=csrf_detected&strategy=oidc

Это происходит даже несмотря на то, что referer указывает на Microsoft, а URI перенаправления корректен.

Примерный User-Agent:

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

Таким образом, хотя интерфейс «подсказывает» Microsoft, фактический браузер — это всё ещё WKWebView Snapchat, а не Safari или ASWebAuthenticationSession.

Перехват через компонент темы не работает

Я попытался смягчить проблему на стороне клиента с помощью компонента темы:
• обнаружение известных User-Agent встроенных браузеров
• блокировка ссылок /auth/oidc
• отображение постоянного оверлея с инструкцией открыть сайт в Safari/Chrome

Однако это не позволяет надёжно перехватить поток, потому что:
• перенаправление OAuth инициируется на стороне сервера
• cookie state должен уже существовать до запуска клиентского JS
• к моменту загрузки темы проблема уже возникает

Поэтому компонент темы не может надёжно предотвратить этот сбой.

Что работает надёжно

Единственное решение, которое работает стабильно, — это переопределение текста сайта:

login.omniauth_error.csrf_detected

чтобы явно объяснить, что:
• вход не удался из-за использования встроенного браузера
• пользователям следует открыть сайт в Safari или Chrome
• а затем повторить вход

Это сообщение отображается на стороне сервера после сбоя и поэтому появляется даже в сломанных контекстах встроенных браузеров.

Это значительно снизило путаницу среди пользователей, поскольку ранее они молча возвращались на страницу входа и часто не осознавали, что что-то пошло не так.

О куки SameSite

Я не переключал параметр same_site_cookies на значение “None”.

Учитывая, что это проблема изоляции WKWebView (а не межсайтовая навигация в Safari), изменение SameSite не устраняет корневую причину и может привести к ненужным компромиссам в области безопасности.

Открытый вопрос

Учитывая вышесказанное, я хотел бы уточнить:
• является ли такое поведение ожидаемым при инициации OAuth из встроенных браузеров iOS
• и планирует ли Discourse поддерживать такой поток или просто документировать, что вход должен выполняться из полноценного браузера

Также было бы полезно, если бы Discourse отображал более явное сообщение по умолчанию для csrf_detected в контекстах OmniAuth, поскольку этот тип сбоя становится всё более распространённым среди студентов, переходящих по ссылкам из Snapchat или Instagram.

Готов предоставить дополнительные анонимизированные логи, если это потребуется — но на данный момент поведение кажется очень последовательным и воспроизводимым.

Спасибо, что уделили время рассмотрению.

Ещё один дополнительный факт, который может помочь подтвердить, что это детерминированное ограничение WKWebView, а не случайное поведение.

Сопоставив логи доступа nginx по множеству сбоев, я обнаружил, что одно и то же значение OIDC state повторно используется встроенным браузером при повторных попытках входа пользователя.

Пример (обезличенный):
• Один и тот же хеш state встречался 14 раз
• Все запросы от:

Snapchat/13.77.0.51 (like Safari…, panda)

• Распределены в течение примерно одного часа повторных попыток входа
• Каждая попытка приводит к:

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

Это явно указывает на то, что:
• браузер никогда не сохраняет успешно cookie сессии, содержащий исходное значение state
• каждая повторная попытка использует тот же устаревший параметр state
• Discourse поэтому корректно отклоняет обратный вызов каждый раз

Напротив, когда тот же пользователь открывает ссылку в Safari, генерируется новое значение state, и вход выполняется успешно сразу.

Таким образом, это выглядит как полностью детерминированный режим сбоя в некоторых встроенных браузерах iOS, а не как проблема синхронизации или гонка условий.

С точки зрения Discourse защита от CSRF работает именно так, как задумано — среда браузера просто не может поддерживать необходимую непрерывность сессии.

Я считаю, что это дополнительно подтверждает:
• что это не то, что можно исправить с помощью компонента темы или клиентского JS
• и что единственное надёжное решение — это серверное информирование и документация

Опубликовал это на случай, если этот факт будет полезен для подтверждения ожидаемого поведения.

Мы переопределили login.omniauth_error.csrf_detected, чтобы явно предупредить о блокировке куки-файлов встроенными браузерами iOS. Пользователи, которые следуют инструкции и повторяют попытку в Safari, сразу же получают доступ, что дополнительно подтверждает: это детерминированное ограничение WKWebView, а не проблема Discourse или IdP.


Спасибо, что дочитали до конца.

На данном этапе мне в основном нужно подтверждение от основных разработчиков того, что:

• OAuth/OIDC, инициируемые во встроенных браузерах iOS (Snapchat, Instagram и т. д.), не являются поддерживаемым или надежным сценарием

• и что ошибки csrf_detected в этом контексте ожидаемы и корректно обрабатываются Discourse

Если это так, я готов рассматривать это как вопрос документации / улучшения сообщений UX на своей стороне, а не как ошибку.

Если есть какие-либо существующие рекомендации, на которые мне стоит ссылаться пользователям (или если Discourse планирует улучшить сообщения об ошибках по умолчанию в этом случае), буду благодарен за указание.