Discourse iOS 앱에서 OIDC 로그인이 가끔 콜백 시 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/state 검증이 실패하여 사용자 계정이 생성되지 않습니다.

주변 로그를 보면 이 문제가 앱 핸드오프(handoff) 흐름에서 발생하고 있는 것으로 보입니다:
application_name=Discourse - iPhone
auth_redirect=discourse://auth_redirect

사용자의 관점에서는 눈에 띄는 현상이 나타나지 않으며, 단순히 로그인 화면으로 되돌아갈 뿐이고 오류를 본 기억이 없는 경우가 많습니다.

Safari나 데스크톱 브라우저를 통해 로그인할 때는 이런 문제가 발생하지 않는 것으로 보입니다.

이는 iOS 쿠키 파티셔닝(cookie partitioning) 또는 인앱 브라우저와 앱 콜백 간의 컨텍스트 전환과 관련이 있을 것으로 추정됩니다.

확인하고 싶은 사항이 있습니다:
• OIDC + iOS 앱 조합에서 이러한 동작이 예상되는지
• 엄격한 정준 HTTPS 오리지널(canonical HTTPS origin)을 보장하는 것 외에 권장되는 완화책이 있는지

도움이 될 수 있다면 익명화된 로그 스니펫을 제공할 수 있습니다.

nginx 액세스 로그에서 추출한 추가 데이터 포인트:

대표적인 실패 사례(2026-01-25 11:44:10 UTC)를 보면, OIDC 콜백 요청이 Discourse iOS 앱의 웹뷰 UA가 아니라 iOS 인앱 브라우저 UA(Snapchat)에서 온 것으로 확인됩니다:

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"으로 변경하는 것인가, 아니면 더 나은 접근 방식이 있는가?

실제 사용 환경에서의 추가 조사와 확인을 바탕으로 후속 보고드립니다.

더 깊이 파고든 결과, 이는 Discourse나 Azure 설정의 문제라기보다는 iOS 인앱 브라우저(WKWebView)의 제한 사항으로 보입니다.

확인된 내용

nginx + Rails 로그 및 사용자 테스트를 통해 확인한 바는 다음과 같습니다:
• OAuth 플로우가 때때로 iOS 인앱 브라우저(예: Snapchat) 내에서 시작됩니다.
• Microsoft 로그인(login.microsoftonline.com)이 해당 인앱 브라우저 내에서 로드됩니다.
• OIDC 콜백은 Discourse에 정상적으로 도달합니다.
• 그러나 state 값을 포함하는 세션 쿠키가 유지되지 않습니다.
• 결국 다음과 같은 결정적인 오류가 발생합니다:

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

이 문제는 리퍼러가 Microsoft이고 리다이렉트 URI가 정확하더라도 발생합니다.

대표적인 UA(User Agent):

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

따라서 UI에서는 Microsoft가 "원박스(onebox)"로 잘 표시되더라도, 실제 기반 브라우저는 Safari나 ASWebAuthenticationSession이 아닌 Snapchat의 WKWebView입니다.

테마 컴포넌트를 통한 인터셉트는 작동하지 않음

클라이언트 측에서 테마 컴포넌트를 사용하여 이를 완화하려 시도했습니다:
• 알려진 인앱 브라우저 UA 감지
• /auth/oidc 링크 차단
• 사용자에게 Safari/Chrome에서 열 것을 안내하는 영구 오버레이 표시

그러나 다음과 같은 이유로 이 방법은 플로우를 안정적으로 인터셉트하지 못했습니다:
• OAuth 리다이렉트는 서버 측에서 시작됩니다.
• state 쿠키는 클라이언트 JS가 실행되기 전에 이미 존재해야 합니다.
• 테마가 로드될 시점에는 이미 문제가 발생한 뒤입니다.

따라서 테마 컴포넌트로는 이러한 실패 모드를 안정적으로 방지할 수 없습니다.

안정적으로 작동하는 방법

일관되게 작동하는 유일한 완화책은 사이트 텍스트를 오버라이드하는 것입니다:

login.omniauth_error.csrf_detected

이를 통해 다음 내용을 명시적으로 설명하도록 설정했습니다:
• 로그인이 인앱 브라우저로 인해 실패했다는 점
• 사용자는 Safari 또는 Chrome에서 사이트를 열어야 한다는 점
• 그 후 로그인을 다시 시도해야 한다는 점

이 메시지는 실패 후 서버 측에서 렌더링되므로, 손상된 인앱 브라우저 컨텍스트에서도 표시됩니다.

이전에는 사용자가 로그인 페이지로 조용히 되돌려지고 무엇이 잘못되었는지 깨닫지 못하는 경우가 많았기 때문에, 이 조치로 사용자 혼란이 크게 줄었습니다.

SameSite 쿠키에 대하여

same_site_cookies를 "None"으로 변경하지 않았습니다.

이것이 WKWebView 격리 문제(Safari의 크로스 사이트 내비게이션이 아님)이기 때문에, SameSite 설정을 변경해도 근본 원인을 해결하지 못할 뿐만 아니라 불필요한 보안 트레이드오프를 초래할 수 있다고 판단했습니다.

미해결 질문

위 내용을 바탕으로 다음 사항에 대해 확인을 요청드립니다:
• OAuth가 iOS 인앱 브라우저에서 시작될 때 이러한 동작이 예상되는 동작으로 간주되는지
• Discourse가 해당 플로우를 지원할 계획인지, 아니면 로그인이 실제 브라우저에서만 이루어져야 한다고 문서화할 것인지

또한 OmniAuth 컨텍스트에서 csrf_detected에 대해 더 명확한 기본 메시지를 Discourse가 제공하면 유용할 것입니다. Snapchat/Instagram 링크를 통해 접속하는 학생 사용자들의 경우 이 실패 모드가 점점 더 흔해지고 있기 때문입니다.

필요하다면 더 많은 익명화 로그를 제공할 수 있습니다. 하지만 이 시점에서 동작은 매우 일관되고 재현 가능합니다.

확인해 주셔서 감사합니다.

이것이 간헐적인 동작이 아니라 결정론적인 WKWebView 제한 사항임을 확인하는 데 도움이 될 수 있는 추가 데이터 포인트가 하나 있습니다.

여러 번의 실패 사례에 걸쳐 nginx 접근 로그를 상관 분석해 보니, 사용자가 로그인을 재시도할 때 인앱 브라우저가 동일한 OIDC 상태(state) 값을 반복적으로 재사용하는 것을 발견했습니다.

예시(정리됨):
• 동일한 상태 해시가 14회 확인됨
• 모든 요청 출처:

Snapchat/13.77.0.51 (like Safari…, panda)

• 약 1시간에 걸친 반복적인 로그인 시도 사이에 분포
• 각 시도는 다음과 같은 결과를 초래함:

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

이는 강력하게 시사합니다:
• 브라우저가 원래 상태를 포함하는 세션 쿠키를 성공적으로 저장하지 못함
• 각 재시도가 동일한 오래된 상태 매개변수를 재사용함
• 따라서 Discourse가 매번 콜백을 올바르게 거부함

반면, 동일한 사용자가 Safari에서 해당 링크를 열면 새로운 상태가 생성되고 즉시 로그인에 성공합니다.

따라서 이는 타이밍이나 레이스 컨디션이 아니라, 특정 iOS 인앱 브라우저에서 완전히 결정론적인 실패 모드로 보입니다.

Discourse의 관점에서, CSRF 보호는 의도대로 정확히 작동하고 있으며, 단순히 브라우저 환경이 필요한 세션 연속성을 유지할 수 없는 것입니다.

저는 이것이 다음을 추가로 뒷받침한다고 생각합니다:
• 이는 테마 컴포넌트나 클라이언트 JS로 완화할 수 있는 문제가 아님
• 그리고 유일한 신뢰할 수 있는 처리 방법은 서버 측 메시징 및 문서화임

이 데이터 포인트가 예상 동작을 확인하는 데 유용할 수 있기를 바라며 게시합니다.

login.omniauth_error.csrf_detected를 오버라이드하여 iOS 인앱 브라우저가 쿠키를 차단하는 문제에 대해 명시적으로 경고하도록 설정했습니다. 해당 지시에 따라 Safari에서 재시도한 사용자는 즉시 성공했으며, 이는 디스코urs나 IdP의 문제가 아니라 WKWebView의 결정적 제한 사항임을 더욱 확인시켜 줍니다.


이 모든 내용을 읽어 주셔서 감사합니다.

현재 저는 핵심 유지보수자로부터 다음 사항에 대한 확인을 주로 요청하고 있습니다:

• iOS 인앱 브라우저(Snapchat/Instagram 등)에서 시작되는 OAuth/OIDC 플로우가 지원되지 않거나 신뢰할 수 없는 플로우라는 점

• 그리고 이 컨텍스트에서 발생하는 csrf_detected 실패가 예상되는 것이며, 디스코urs가 이를 올바르게 처리하고 있다는 점

이것이 사실이라면, 저는 이를 버그가 아니라 제 쪽의 문서/UX 메시징 문제로 취급하는 데 동의할 수 있습니다.

사용자에게 안내해야 할 기존 가이드라인이 있거나(또는 디스코urs가 이 부분의 기본 오류 메시지를 개선할 계획이 있다면) 해당 정보를 알려 주시면 감사하겠습니다.