فشل تسجيل الدخول عبر 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) فشل المصادقة! csrf_detected

يصل رد الاتصال إلى Discourse، لكن تحقق CSRF/state يفشل، لذلك لا يتم إنشاء أي حساب مستخدم.

تشير السجلات المحيطة إلى أن هذا يحدث في تدفق تسليم التطبيق:

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

من وجهة نظر المستخدم، لا يظهر شيء واضح - تتم إعادته ببساطة إلى شاشة تسجيل الدخول وغالباً لا يتذكر رؤية خطأ.

لا يبدو أن هذا يحدث عند تسجيل الدخول عبر متصفح Safari أو متصفحات سطح المكتب.

افتراضي هو أن هذا يتعلق بتقسيم ملفات تعريف الارتباط في نظام iOS / التبديل السياقي بين المتصفح داخل التطبيق ورد اتصال التطبيق.

أردت فقط التحقق من صحة ما يلي:

  • ما إذا كان هذا هو السلوك المتوقع مع OIDC + تطبيق iOS
  • وما إذا كانت هناك أي تخفيفات موصى بها بخلاف التأكد من أصل HTTPS قياسي صارم

شكراً - يسعدني تقديم مقتطفات سجلات مجهولة المصدر إذا كانت مفيدة.

نقطة بيانات إضافية من سجلات وصول nginx:
تُظهر محاولة فشل تمثيلية (2026-01-25 11:44:10 بالتوقيت العالمي المنسق) أن طلب استدعاء OIDC يأتي من وكيل مستخدم (User Agent) متصفح داخل التطبيق لنظام 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 التي تقوم بعد ذلك بالربط العميق (deep-link) إلى تطبيق Discourse؟
هل سيكون تغيير same_site_cookies إلى “None” هو التخفيف الموصى به هنا، أم أن هناك نهجًا أفضل؟

متابعة لبعض التحقيقات الإضافية والتأكيد من الاستخدام الفعلي.

بعد التعمق أكثر، أعتقد أن هذه بالفعل قيود متصفح داخل التطبيق لنظام iOS (WKWebView) وليست شيئًا خاصًا بـ Discourse أو تكوين Azure.

ما تمكنت من تأكيده

من سجلات nginx + Rails واختبارات المستخدمين:

  • تبدأ عملية OAuth أحيانًا داخل متصفح داخل تطبيق iOS (مثل Snapchat).
  • يتم تحميل تسجيل دخول Microsoft (login.microsoftonline.com) داخل متصفح التطبيق الداخلي هذا.
  • يصل رد نداء OIDC إلى Discourse بنجاح.
  • لكن ملف تعريف الارتباط الخاص بالجلسة الذي يحتوي على قيمة الحالة لا يبقى.
  • مما يؤدي إلى نتيجة حتمية:
GET /auth/failure?message=csrf_detected&strategy=oidc

يحدث هذا على الرغم من أن المُحيل هو Microsoft وأن عنوان URI لإعادة التوجيه صحيح.

وكيل مستخدم ممثل:

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.

اعتراض مكون السمة لا يعمل

لقد حاولت التخفيف من هذا الأمر من جانب العميل باستخدام مكون سمة:

  • اكتشاف وكلاء المستخدمين المعروفين للمتصفح داخل التطبيق.
  • حظر روابط /auth/oidc.
  • عرض تراكب دائم يوجه المستخدمين إلى الفتح في Safari/Chrome.

ومع ذلك، فإن هذا لا يعترض التدفق بشكل موثوق، لأن:

  • يتم بدء إعادة توجيه OAuth من جانب الخادم.
  • يجب أن يكون ملف تعريف الارتباط الخاص بالحالة موجودًا بالفعل قبل تشغيل JavaScript العميل.
  • بحلول وقت تحميل السمة، يكون الضرر قد وقع بالفعل.

لذا، لا يمكن لمكون السمة منع وضع الفشل هذا بشكل موثوق.

ما يعمل بشكل موثوق

التخفيف الوحيد الذي وجدته ويعمل باستمرار هو تجاوز نص الموقع:

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 يتم إعادة استخدامها مرارًا وتكرارًا بواسطة المتصفح داخل التطبيق عندما يحاول المستخدمون تسجيل الدخول مرة أخرى.

مثال (تم تنقيحه):
• نفس تجزئة الحالة شوهدت 14 مرة
• جميع الطلبات من:

Snapchat/13.77.0.51 (like Safari…, panda)

• موزعة على مدار ساعة تقريبًا من محاولات تسجيل الدخول المتكررة
• كل محاولة تؤدي إلى:

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

هذا يشير بقوة إلى أن:
• المتصفح لا يقوم أبدًا بتخزين ملف تعريف الارتباط الخاص بالجلسة الذي يحتوي على الحالة الأصلية بنجاح
• كل محاولة إعادة استخدام تعيد استخدام نفس معلمة الحالة القديمة
• لذلك، يرفض Discourse بشكل صحيح رد الاتصال في كل مرة

في المقابل، عندما يفتح المستخدم نفس الرابط في سفاري، يتم إنشاء حالة جديدة وينجح تسجيل الدخول على الفور.

لذا يبدو أن هذا وضع فشل حتمي بالكامل في بعض متصفحات iOS داخل التطبيق بدلاً من كونه حالة توقيت أو سباق.

من منظور Discourse، فإن حماية CSRF تتصرف تمامًا كما هو مقصود - فبيئة المتصفح ببساطة لا يمكنها الحفاظ على استمرارية الجلسة المطلوبة.

أعتقد أن هذا يدعم أيضًا ما يلي:
• هذا ليس شيئًا يمكن لمكون سمة (theme component) أو JavaScript العميل تخفيفه
• وأن المعالجة الموثوقة الوحيدة هي المراسلة من جانب الخادم والتوثيق

أنشر هذا في حال كانت نقطة البيانات هذه مفيدة لتأكيد السلوك المتوقع.