شرح OAuth 2.1 و OIDC: الدليل العملي للتفويض والمصادقة الآمنة
OAuth بروتوكول تفويض لا مصادقة، ومن هذا الخلط تبدأ معظم ثغرات الهوية. إليك النموذج الذهني الصحيح، والتدفقات المهمة، وقائمة التحقق التي تصمد فعلاً في الإنتاج.
بقلم Innovation T Team
مع تسارع التحول الرقمي لدى الشركات في تونس والمنطقة، من بوابات الدفع إلى تطبيقات الخدمات، أصبحت طبقة الهوية أول ما يطرقه المهاجمون. وهنا خلط شائع يجب حسمه فوراً: OAuth بروتوكول تفويض (delegation)، وليس بروتوكول مصادقة. هذا الالتباس وحده يقف وراء نصف ثغرات الهوية التي نكتشفها في مراجعاتنا الأمنية. إذا كان فريقك يبني أزرار تسجيل دخول، أو يصدر رموز API، أو يشغل خدمات تتخاطب آلياً فيما بينها، فالدقائق العشر القادمة قد توفر عليك حادثة أمنية كاملة.
صحح النموذج الذهني أولاً
يجيب OAuth 2.x عن سؤال واحد بالضبط: هل يحق لهذا التطبيق استدعاء هذه الواجهة (API)، وربما نيابة عن مستخدم معين؟ وهو لا يقول شيئاً موثوقاً عن هوية ذلك المستخدم. أما OpenID Connect (اختصاراً OIDC) فهو طبقة الهوية المبنية فوقه: يضيف رمز الهوية ID token، ونقطة نهاية UserInfo، وقواعد صارمة تتيح للتطبيق العميل التأكد من أن حدث مصادقة حقيقياً قد وقع فعلاً.
الفرق ليس أكاديمياً. إذا تعامل الخادم الخلفي لديك مع "استلمت access token" على أنها "هذا المستخدم مصادق عليه"، فأي تطبيق حصل بشكل مشروع على رمز لذلك المستخدم يستطيع إعادة تمريره إلى واجهتك وانتحال شخصيته. مشكلة إعادة تمرير رموز الوصول هذه هي بالضبط سبب وجود OIDC. التفويض والمصادقة ادعاءان مختلفان، ويحتاجان رموزاً مختلفة.
علق هذه الجملة على حائط الفريق: OAuth يحدد ما يجوز للتطبيق فعله، و OIDC يحدد من هو المستخدم.
ما الذي يغيره OAuth 2.1 فعلياً
لا يزال OAuth 2.1 مسودة لدى IETF، لكن تعامل معه كخط الأساس. فهو ببساطة OAuth 2.0 بعد دمج عقد كامل من الدروس الأمنية، ومعظمها وثق سابقاً في RFC 9700، وثيقة أفضل الممارسات الأمنية لبروتوكول OAuth. البناء وفق 2.1 اليوم يعني تطبيق 2.0 بشكل صحيح، لا أكثر.
التغييرات الملموسة:
- انتهى عهد implicit grant. الرموز المسلمة في أجزاء URL (fragments) كانت تتسرب عبر سجل المتصفح وترويسات referrer ووسطاء التسجيل، ولم يكن هناك سبيل لإصلاح ذلك، فحذف التدفق بالكامل.
- انتهى كذلك resource owner password grant. أي تدفق يعود المستخدمين على كتابة كلمة مرورهم داخل تطبيق طرف ثالث هو برنامج تدريبي على التصيد.
- أصبح PKCE إلزامياً في كل تدفق authorization code، بما في ذلك العملاء السريون (confidential clients). فهو يقضي على اعتراض رمز التفويض ويضيف حماية من CSRF دون كلفة.
- عناوين إعادة التوجيه (redirect URIs) يجب أن تتطابق حرفياً. لا wildcards، ولا مطابقة بالبادئة، ولا "أي شيء تحت هذا النطاق الفرعي".
- رموز التحديث (refresh tokens) للعملاء العموميين يجب أن تكون للاستخدام مرة واحدة (بالتدوير) أو مقيدة بالمرسل.
- ممنوع تمرير رموز bearer في سلاسل الاستعلام (query strings)، لأنها تبقى في سجلات الوصول إلى الأبد.
إذا كان مزود الهوية لديك، أو تنفيذك الخاص، يخالف أياً من هذه النقاط، فهذه هي قائمة الإصلاحات عندك، مرتبة حسب الأولوية.
الأجزاء المتحركة، بدقة
أربعة أدوار:
- مالك المورد (resource owner): الإنسان صاحب البيانات.
- العميل (client): التطبيق الذي يطلب الوصول (تطبيق SPA، تطبيق جوال، خدمة خلفية).
- خادم التفويض (authorization server، اختصاراً AS): يصدر الرموز بعد مصادقة المستخدم وتسجيل موافقته. أمثلة: Keycloak و Auth0 و Entra ID و Cognito و Zitadel.
- خادم الموارد (resource server، اختصاراً RS): واجهة API لديك، وهي التي تقبل رموز الوصول وتتحقق منها.
ثلاثة رموز بوظائف مختلفة تماماً:
- رمز الوصول (access token): بيان اعتماد موجه لخادم الموارد، قصير العمر. على العميل معاملته كسلسلة مبهمة حتى لو صادف أنه JWT.
- رمز التحديث (refresh token): بيان اعتماد طويل العمر يستبدله العميل برموز وصول جديدة دون إزعاج المستخدم. وهو أثمن ما يمكن للمهاجم سرقته.
- رمز الهوية (ID token، خاص بـ OIDC): رمز JWT موجه إلى العميل لا إلى أي API. يؤكد أن "هذا المستخدم صادق في هذا الوقت وبهذه الطريقة"، وجمهوره (audience) هو client_id الخاص بتطبيقك.
قاعدتان تمنعان فئات كاملة من الثغرات:
- رموز الهوية لا تعبر حدود أي API أبداً. يستهلكها العميل وتنتهي مهمتها.
- العميل لا يتخذ قرارات عبر تفكيك رموز الوصول. محتوى الرمز عقد بين خادم التفويض وخادم الموارد.
التدفقات التي تهمك في 2026
تحتاج أربعة تدفقات، وكل ما عداها إرث قديم.
Authorization code مع PKCE
الخيار الافتراضي لكل ما هو تفاعلي: تطبيقات الويب و SPA والجوال. ينشئ العميل سراً عشوائياً (verifier)، ويرسل بصمته بخوارزمية SHA-256 (challenge) مع طلب التفويض، ثم يثبت حيازته للسر عند استبدال الرمز. المهاجم الذي يعترض الرمز لا يستطيع استبداله.
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest(
"SHA-256", new TextEncoder().encode(verifier)
);
const challenge = base64url(new Uint8Array(digest));
يجب أن يحمل طلب التفويض دائماً response_type=code، و code_challenge مع code_challenge_method=S256، وقيمة state تتحقق منها عند العودة، وفي حالة OIDC أضف scope=openid مع nonce تتأكد منه داخل رمز الهوية. إسقاط state أو nonce بحجة أن "PKCE يغطي الأمر" اختصار شائع. صحيح أن PKCE يغطي معظمه، لكن الدفاع في العمق لا يكلفك سوى سلسلتين عشوائيتين.
Client credentials
للتخاطب الآلي بين الخدمات دون أي مستخدم: خدمة فوترة تستدعي API للفواتير، أو خدمة إشعارات ترسل تأكيدات الطلبات لعملائك عبر WhatsApp Business API كما تفعل أغلب المتاجر الإلكترونية في المنطقة، أو مهمة cron تسحب التقارير ليلاً. يصادق العميل بأوراق اعتماده الخاصة ويحصل على رمز مقيد بنطاقه هو. انضباطان مهمان هنا: اطلب رمزاً لجمهور (audience) محدد لا رمزاً شاملاً، واحفظ سر العميل في خزنة أسرار (vault) مع تدوير دوري، لا في ملف بيئة أودع في المستودع "مؤقتاً".
Device authorization grant
للأجهزة محدودة الإدخال: الشاشات الذكية، وأدوات سطر الأوامر CLI، وأكشاك الخدمة الذاتية التي بدأنا نراها في الفروع البنكية ومراكز الخدمات في تونس والخليج. يعرض الجهاز رمزاً قصيراً، يوافق المستخدم من هاتفه، ثم يستطلع الجهاز نقطة النهاية حتى يحصل على الرمز. إن سبق لك أن كتبت رمزاً في github.com/login/device فقد استخدمت هذا التدفق.
Token exchange
معياره RFC 8693، وهو مخصص لسلاسل الخدمات. تتلقى الخدمة A رمز مستخدم وتحتاج إلى استدعاء الخدمة B نيابة عنه. النمط الخاطئ هو تمرير الرمز الأصلي عبر خمس خدمات متتالية، كل منها يقبل رمزاً لم يوجه إليها أصلاً. يتيح token exchange للخدمة A استبدال الرمز الوارد برمز جديد مقيد بالخدمة B، مع تسجيل سلسلة التفويض في claim باسم act. من واقع خبرتنا، هذه هي القطعة الغائبة في معظم بيئات microservices بالمنطقة، وهي السبب في أن رمزاً مسروقاً واحداً كثيراً ما يفتح منصة بأكملها.
تحقق من الرموز بجدية
تأتي رموز الوصول بصيغتين. الرموز المبهمة تتطلب استدعاء نقطة نهاية introspection (وفق RFC 7662): زمن استجابة أعلى مقابل إبطال فوري. أما رموز JWT فتتحقق محلياً مقابل المفاتيح المنشورة لخادم التفويض (JWKS): سرعة عالية، لكن الإبطال لا يسري إلا عند انتهاء الصلاحية. الحل الوسط المتعارف عليه: رموز JWT بعمر من 5 إلى 15 دقيقة مع تدوير رموز التحديث، وحجز introspection للعمليات عالية الحساسية.
التحقق المحلي من JWT هو المكان الذي تنزف فيه المراجعات الأمنية. طبق هذه القائمة على كل خادم موارد، وفي كل مرة:
- اجلب مفاتيح التوقيع من نقطة نهاية JWKS، واختر المفتاح عبر
kid، وخزنها مؤقتاً بمدة TTL معقولة. لا تثبت المفاتيح في الشيفرة أبداً. - ثبت قائمة خوارزميات مسموحة. توقع
RS256أوES256وارفض ما عداهما. هذا السطر الواحد يقضي علىalg: noneوعلى هجوم الخلط الشهير بين RS256 و HS256 معاً. - تحقق من
issمقابل عنوان المصدر الحرفي. https، ودون مفاجآت الشرطة المائلة في نهاية العنوان. - تحقق من أن
audيتضمن معرف واجهة API الخاصة بك. الرمز الصالح لواجهة غيرك ليس رمزاً صالحاً لواجهتك. - افرض
expوnbfمع هامش انحراف زمني صغير، من 60 إلى 120 ثانية. - لرموز الهوية، تحقق إضافياً من تطابق
nonceمع ما أرسلته، ومنazpعند وجود أكثر من جمهور. - ثم فوض. صحة التوقيع تعني أن خادم التفويض أصدر الرمز، لا أن هذا المستدعي يحق له حذف هذا السجل. تحقق من scopes والأدوار عند كل نقطة نهاية.
في ASP.NET Core يختصر معظم ذلك إلى إعدادات:
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "https://id.example.com",
ValidAudience = "api://orders",
ValidAlgorithms = new[] { "RS256" },
ClockSkew = TimeSpan.FromSeconds(60)
};
توجد إعدادات مكافئة في مكتبة jose لبيئة Node، و spring-security-oauth2-resource-server لجافا، و authlib لبايثون. استخدم مكتبة مصانة. تفكيك JWT يدوياً هو الطريق المعتاد إلى حوادث alg: none. وللاطلاع على الصورة الأشمل حول تحصين واجهاتك، راجع دليلنا حول أفضل ممارسات أمن API.
أين تعيش الرموز في المتصفح
الحقيقة المزعجة: لا يوجد مكان آمن تماماً للرموز في JavaScript داخل المتصفح. يصمد localStorage أمام XSS بقدر الوقت اللازم لسرقة محتواه فقط. الرموز المحفوظة في الذاكرة أفضل، لكنها تزول مع تحديث الصفحة، وتسقط أيضاً أمام حقن نصي يخطف عميل HTTP لديك.
النمط الذي نوصي به لأي مشروع جاد هو Backend for Frontend (اختصاراً BFF). تجري رقصة OAuth كاملة على الخادم، ولا تصل الرموز إلى المتصفح إطلاقاً. يحصل تطبيق SPA على كوكي بخصائص HttpOnly و Secure و SameSite مرتبط بجلسة خادم، ويتولى BFF إرفاق رمز الوصول بالاستدعاءات الصاعدة. يبقى بإمكان XSS استغلال الجلسة ما دامت الصفحة مفتوحة، لكنه لم يعد قادراً على سرقة refresh token والرحيل بوصول دائم.
وأينما عاشت رموز التحديث، دورها. كل عملية تحديث تصدر رمز تحديث جديداً وتبطل القديم. وإذا قدم رمز مستعمل سابقاً مرة أخرى، فتلك إشارة السرقة: أبطل عائلة الرموز بأكملها وافرض إعادة المصادقة. معظم المزودين الناضجين يدعمون ذلك جاهزاً، لكنه معطل افتراضياً أكثر مما تتوقع. أما الرموز المقيدة بالمرسل عبر DPoP فتذهب خطوة أبعد بربط الرموز بمفتاح يحتفظ به العميل، ودعمها في نمو مطرد.
أنماط الفشل التي نصادفها باستمرار
- استخدام رموز الهوية كبيانات اعتماد لواجهات API. يقبل خادم الموارد أي JWT بتوقيع صحيح دون فحص
aud. العلاج: فحص الجمهور في كل مكان. - مطابقة متساهلة لعناوين إعادة التوجيه. wildcard واحد زائد open redirect واحد على أي نطاق فرعي مطابق يساوي رموز تفويض مسروقة.
- غياب
stateوnonce. هجمات CSRF على تسجيل الدخول وتثبيت للجلسات، ثغرات قابلة للاستغلال بهدوء لسنوات. - رمز واحد شامل لكل الخدمات. لا رموز لكل جمهور، ولا token exchange، وأي pod مخترق يستطيع استدعاء أي شيء. هذا بالضبط هو الفشل الذي صممت بنية Zero Trust لاحتوائه.
- رموز وصول بعمر 24 ساعة دون أي خطة إبطال. حين يسرق حاسوب محمول، "ننتظر إلى الغد" ليست خطة استجابة للحوادث.
- تطبيقات جوال تعتمد مخططات URI مخصصة لإعادة التوجيه. أي تطبيق مثبت يمكنه تسجيل المخطط نفسه واعتراض الرمز. استخدم عناوين https موثقة: App Links على Android و Universal Links على iOS.
- أسرار عملاء مضمنة داخل تطبيقات SPA وحزم تطبيقات الجوال. العملاء العموميون لا يستطيعون حفظ الأسرار، ولهذا وجد PKCE أصلاً.
تبني أم تشتري أم تستضيف بنفسك
لا تبن خادم تفويض خاصاً بك أبداً. سطح البروتوكول (نقاط نهاية الرموز، شاشات الموافقة، تدوير المفاتيح، إدارة الجلسات، الإبطال) هائل ومعاد. القرار الحقيقي هو: خدمة مدارة أم استضافة ذاتية.
- المدارة (Auth0 و Cognito و Entra External ID): أسرع طريق إلى الإنتاج، بإعدادات افتراضية قوية، لكن التسعير لكل مستخدم نشط، وغالباً بالدولار، يبدو زهيداً في البداية ثم يصبح مؤلماً مع النمو، خاصة للشركات التي تفوتر عملاءها بالدينار أو بعملة محلية أخرى. من خبرتنا، نقاش التسعير يبدأ عادة عند عشرات الآلاف من المستخدمين النشطين شهرياً.
- الاستضافة الذاتية (Keycloak و Zitadel و Ory Hydra و Authentik): تحكم كامل، وسيادة على البيانات، ولا رسوم لكل مستخدم. مسألة حاسمة للبنوك وشركات الاتصالات والجهات الخاضعة لمتطلبات إقامة البيانات محلياً في تونس ودول الخليج. في المقابل، أنت من يملك الترقيات والتحصين والتوافرية وإدارة المفاتيح. خصص وقتاً هندسياً حقيقياً، لا عطلة نهاية أسبوع.
مهما اخترت، اشترط ما يلي: شهادة OIDC معتمدة، دعم PKCE وتدوير رموز التحديث، جماهير مخصصة لكل API، أعمار رموز قصيرة تتحكم فيها بنفسك، ودعم WebAuthn لأن تسجيل الدخول بكلمة المرور في طريقه إلى الزوال. وإذا كانت passkeys على خارطة طريقك، وينبغي أن تكون، فسيبقى OIDC هو قناة التوصيل: مقالنا حول مفاتيح المرور والمصادقة دون كلمات مرور يشرح كيف تتكامل القطع.
لم يعد البروتوكول هو الجزء الصعب. الصعب هو الانضباط: عناوين إعادة توجيه حرفية، PKCE إلزامي، رموز مقيدة بالجمهور، أعمار قصيرة، تدوير مع كشف إعادة الاستخدام، وقوائم تحقق تفرض في مراجعات الشيفرة. الفرق التي ترسخ هذه العادات الست تتوقف ببساطة عن التعرض لحوادث OAuth.
كيف تساعدك Innovation T
تصمم Innovation T بنى الهوية الرقمية وتبنيها كتخصص أساسي: تكاملات OIDC، وبنى BFF لتطبيقات SPA، ونشر Keycloak ومزودي الهوية السحابيين، وتطبيق token exchange في بيئات microservices، وتدقيق تطبيقات OAuth القائمة مقابل خط أساس 2.1. رأينا أنماط الفشل أعلاه ميدانياً لدى شركات في تونس والمنطقة، ونعرف كيف نغلقها دون كسر جلسات مستخدميك.
سواء كنت تختار مزود هوية، أو تفكك تدفق implicit موروثاً، أو تحصن منظومة API كاملة، استكشف خدماتنا أو تحدث إلى فريقنا. سنقول لك بصراحة ما الذي يجب إصلاحه أولاً.
جاهز للبناء مع Innovation T؟
سواء كان الأمر يتعلق بالأمن أو النمو أو الهندسة، يمكن لفريقنا مساعدتك على تنفيذه بإتقان.