← All posts

BFF لمستأجرين كثيرين، والطلب الوحيد الذي يصل بلا كوكي

Authagonal·July 28, 2026

لُقِّنت تطبيقات الصفحة الواحدة أن تحتفظ برموزها بنفسها. يُجري التطبيق تدفق OAuth داخل المتصفح، فيحصل على رمز وصول وغالبًا رمز تجديد، ويحتفظ بهما في مكان ما داخل JavaScript، ويرفقهما بكل نداء إلى الـ API. الأمر موثَّق جيدًا، وهو ما تعرضه معظم الشروحات، وهو يضع أطول بيانات اعتمادك عمرًا في المكان الوحيد الذي لا تستطيع الدفاع عنه: بيئة تشغيل تنفّذ بكل رحابة صدر أي شيء يجد طريقه إلى الصفحة. حقنة ناجحة واحدة، أو تبعية واحدة مخترَقة في مكان ما من بنائك، ولا يحتاج أحد إلى التصيّد. يقرؤون الرمز ويمضون، ويظل يعمل حتى ينتهي أجله.

أما نمط backend-for-frontend فينقل بيانات الاعتماد إلى حيث لا تُنال. يتحدث المتصفح إلى خادم صغير يخصّ تطبيقك، وهذا الخادم هو عميل OAuth السرّي. هو من يُجري تبادل الشيفرة، وهو من يحتفظ برمزَي الوصول والتجديد، ولا يسلّم المتصفح سوى كوكي مبهم. هذا وصف للطريقة التي بنينا بها خادمنا، وللجزء الوحيد الذي تبيّن أنه مثير للاهتمام فعلًا: جعله يخدم أكثر من مستأجر واحد.

ما ينتهي به المتصفح

كوكي اسمه __Host-agbff، موسوم بـ HttpOnly وSameSite=Lax، نطاقه المسار كله، بلا تاريخ انتهاء فيموت بموت جلسة المتصفح. قيمته 256 بتًا من العشوائية ولا شيء غير ذلك. إنه ليس رمزًا، ولا يُفكّ إلى أي شيء، وسرقته من على السلك ليست بالأمر الذي يمكنك فعله بكوكي __Host- عبر TLS.

كل ما هو حقيقي يقبع على جانب الخادم، في سجل جلسة داخل ذاكرة تخزين مؤقت موزَّعة: رمز الوصول، ورمز التجديد، ورمز الهوية، ومتى ينتهي أجل رمز الوصول، وأي مستأجر تعود إليه الجلسة. أما تسجيل الدخول نفسه فهو تدفق شيفرة تفويض عادي مع PKCE، يُجريه عميل سرّي يستوثق لدى نقطة نهاية الرمز بسرّه. ويُتحقق من رمز الهوية كما ينبغي عند العودة: المُصدِر، والجمهور، والتوقيع مقابل المفاتيح المنشورة، والعمر، ثم مقارنة بزمن ثابت للـ nonce مقابل القيمة المخبّأة قبل إعادة التوجيه. وبعد كل ذلك فقط توجد جلسة ويُضبط كوكي.

ترويسة يُفحَص وجودها فقط، لا غير

يستدعي المتصفح الـ BFF ليطلب معلومات مستخدمه ذاته وليصل إلى الـ API الموكَّل، وتحمل تلك النداءات ترويسة مخصّصة، x-authagonal-bff. قيمتها لا تهم. الوجود هو الفحص كله.

يبدو ذلك تكاسلًا وهو ليس كذلك. صنف تزوير الطلبات عبر المواقع بأكمله يتّكئ على إرسال نموذج، أو وسم صورة، أو تنقّل يستطيع أصل آخر إطلاقه بينما يمتطي كوكيك الطلب معه، ولا شيء من ذلك يقدر على ضبط ترويسة مخصّصة. وفي اللحظة التي تحاول فيها الـ JavaScript المهاجِمة إضافة واحدة يكفّ الطلب عن كونه بسيطًا ويصير طلبًا مسبوقًا بفحص مبدئي، وهو ما ترفضه سياسة CORS لديك. الترويسة ليست سرًّا يُخمَّن، بل هي دليل على أن الطلب جاء من شيفرة لا من ترميز.

وهي مطلوبة في النداءات المكتوبة برمجيًا، وغير مطلوبة عمدًا في تلك التي هي بطبيعتها تنقّلات عليا المستوى: بدء تسجيل دخول، أو العودة من مزوّد الهوية، أو اتّباع رابط تسجيل خروج. فاشتراط ترويسة مخصّصة على تنقّل في المتصفح لن يفعل سوى كسر التنقّل.

التجديد، مرة واحدة بالضبط

قد يكتشف كل طلب موكَّل أن رمز الوصول على وشك أن ينتهي أجله، والصفحة المزدحمة تُطلق عدة طلبات في آن. جدِّدها كلها بسذاجة تحصل على كارثة صغيرة: عدة تجديدات متزامنة برمز تجديد دوّار، كلٌّ منها يُبطل الأخرى، وتنتهي بخروج المستخدم بفعل حركته هو.

لذا فالتجديد أحادي الطيران، لكل جلسة على حدة. أول طلب يمرّ يأخذ بوابة مفتاحها معرّف الجلسة وينتظر الجميع سواه. والجزء الدقيق هو ما يفعله المنتظرون حين يدخلون: يعيدون قراءة الجلسة من المخزن قبل أن يقرروا أي شيء، لأن الطلب الذي أمسك بالبوابة أولًا قد جدّد على الأرجح بالفعل، والقيمة التي كانوا يحملونها حين اصطفّوا صارت قديمة. جدِّد مرة، ثم يستخدم الجميع النتيجة.

وهو أيضًا واعٍ للتدوير بالمعنى الممل الضروري. إذا أعادت نقطة نهاية الرمز رمز تجديد جديدًا، حلّ محل المخزَّن. وإذا فشل التجديد بخطأ في الرمز، تُحذف الجلسة ويُخرَج المستخدم، لأن رمز تجديد قد رُفض لن يُقبل في المحاولة التالية، وإعادة المحاولة ليست سوى طريقة أبطأ للخروج.

BFF واحد، مستأجرون كثيرون

الـ BFF أحادي المستأجر يستمدّ كل شيء من الضبط عند الإقلاع: مرجع واحد، ومعرّف عميل واحد، وسرّ واحد. وخدمة مستأجرين كثيرين من عملية نشر واحدة تكسر ذلك، لأن على الـ BFF الآن أن يستبين إلى أي مُصدِر يعود تسجيل دخول ما، وعليه أن يفعل ذلك قبل أن توجد أي جلسة.

يحدث الاختيار عند نقطة نهاية تسجيل الدخول، عبر معامل استعلام تختار اسمه بنفسك، فقد يكون ?slug= أو ?org= أو أيًّا كان ما يسمّي به منتجُك عميلَه أصلًا. ويحوّل مُحلِّلٌ ذلك المفتاح إلى ضبط مستأجر: مرجع، ومعرّف عميل، وسرّ، ونطاقات. وإن لم يُحلّ إلى شيء، رُفض تسجيل الدخول عندها وهناك.

ثم على المفتاح أن ينجو من رحلة ذهاب وعودة إلى مزوّد هوية لا يعرف عنه شيئًا، ثم يعود. لا جلسة بعدُ لتحمله، فيمتطي كوكي الترابط، ذلك الكوكي المشفَّر قصير العمر الذي يحمل أصلًا مُتحقِّق PKCE والحالة والـ nonce لهذا السبب بعينه. خمس عشرة دقيقة، واحد لكل محاولة تسجيل دخول. وفي طريق العودة يُفكّ تشفير الكوكي، ويُحلّ المستأجر مجددًا من المفتاح داخله، وتُبادَل الشيفرة مقابل نقطة نهاية رمز ذلك المستأجر. وعندها فقط يُكتب مفتاح المستأجر في الجلسة، حيث يصير الجواب الدائم.

ومن تلك اللحظة يعيد كل مسار الحلّ من الجلسة لا من أي شيء قاله المتصفح: التجديد، وتسجيل الخروج، والوكيل. وحين يكفّ مستأجر عن الانحلال، لأنه حُذف أو عُطِّل، تُدمَّر الجلسة بدلًا من التراجع بهدوء إلى مستأجر افتراضي ما. المستأجر الذي لا يمكن حلّه هو خروج، لا هزّة كتفين.

الطلب الذي يصل بلا كوكي

تسجيل الخروج عبر القناة الخلفية هو حيث يثبت هذا التصميم جدواه. يُرسل مزوّد الهوية رمز خروج بطلب POST مباشرةً إلى خادمك حين تنتهي جلسة في مكان آخر. لا كوكي. لا جلسة. لا متصفح البتة. في BFF أحادي المستأجر هذا أمر عادي، إذ لا يوجد سوى مُصدِر واحد قد يأتي منه. أما في BFF متعدد المستأجرين فعليك أن تجيب عن «لأي مستأجر هذا؟» قبل أن تجيب عن «أهذا حقيقي أصلًا؟»، والمادة الوحيدة المتوفرة هي الرمز نفسه، الذي لم تتحقق منه وبالتالي لا يمكنك الوثوق به.

الترتيب الذي يجعل هذا آمنًا يستحق أن يُذكر بدقة. اقرأ ادعاء المُصدِر من الرمز غير المتحقَّق منه، واستخدمه لشيء واحد فقط: اختيار ضبط أي مستأجر تُحمِّل. ثم تحقق من توقيع الرمز مقابل المفاتيح المنشورة لذلك المستأجر ومن معرّف عميله بوصفه الجمهور. المُصدِر المزوَّر يختار المستأجر الذي يتظاهر بأنه هو، ثم يفشل في التحقق مقابل مفاتيح ذلك المستأجر، لأن المهاجم لا يملكها. لم يُقبل أي شيء قط بناءً على قوة الادعاء غير الموثوق. الادعاء لم يقرر سوى مَن يحقّ له أن يحكم عليه، والحاكم مرتكز ثقة كان لدينا أصلًا.

وبعد ذلك تأتي الفحوص التي تجعل رمز الخروج رمز خروج: يجب ألّا يحمل nonce، إذ إنه لا يستوثق من أحد؛ ويجب أن يحمل حدث تسجيل الخروج عبر القناة الخلفية؛ ويجب أن يحدد إما جلسة بعينها أو ذاتًا. معرّف الجلسة يقتل جلسة واحدة. والذات تقتل كل جلسة يملكها ذلك المستخدم، وهي الصيغة التي نصدرها، لأن «سجّل خروجي من كل مكان» هو ما يعنيه الناس فعلًا حين ينقرون على تسجيل الخروج من كل مكان. ولهذا يحتفظ مخزن الجلسات بفهرس حسب معرّف الجلسة وآخر حسب الذات، لا حسب قيمة الكوكي وحدها.

المستأجر الواحد يبقى مملًّا

كل هذا خامل في الحالة الشائعة. المُحلِّل الافتراضي يعيد الضبط نفسه أيًّا كان المفتاح الذي يُسلَّم له، وحلّه حسب المُصدِر يعيد ذلك الضبط دون شرط، لأنه لا يوجد سوى واحد. الـ BFF أحادي المستأجر يتصرف تمامًا كما كان قبل وجود الفاصل: لا معامل مستأجر، ولا شيء إضافي في الكوكي، ولا أنماط فشل جديدة. آلية تعدد المستأجرين اختيارية وغير مرئية إلى أن تسمّي معامل استعلام.

افصل الاختيار عن الثقة

حين تجعل شيئًا متعدد المستأجرين، فالتمرين الجدير بالفعل هو أن تُحصي كل نقطة دخول وتسأل ما الذي يحدد المستأجر على كل واحدة منها. تنقّلات المتصفح تحمل كوكيًا أصدرته أنت وتتحكم به. ردود التوجيه تحمل حالة وقّعتها أنت. أما ردود الخادم إلى الخادم فلا تحمل سوى ما اختار المتصل إرساله، وقد يكون المتصل يكذب.

نقطة الدخول المُحرِجة، تلك التي لا شيء جدير بالثقة عليها، هي التي يقرر تصميمها أسليم النظام كله أم لا، وهي عادةً التي لا يفكر فيها أحد إلا متأخرًا. النمط الذي أخرجنا منها يُعمَّم: افصل الاختيار عن الثقة. لا بأس بأن توجّه بناءً على بيانات غير موثوقة ما دام التوجيه يختار فقط أي مرتكز ثقة يُسمح له باتخاذ القرار النهائي، والقرار النهائي يتخذه شيء لا يستطيع المهاجم تزويره.

وإن كنت تفضّل ألّا تكتب كل ذلك بنفسك، فإن Authagonal تقدّم الـ BFF لـ .NET ولـ Node، وستهيّئ البوابة العميل السرّي الذي يحتاجه، بعنوان إعادة التوجيه ونقطة نهاية تسجيل الخروج عبر القناة الخلفية وكل شيء، بنقرة واحدة.