مفاهيم OAuth 2.0 للمختبِرين
يتيح OAuth 2.0 (المعرَّف في RFC 6749) لتطبيقٍ ما وصولاً محدوداً إلى واجهة API دون أن يرى كلمة مرور المستخدم. يسجّل المستخدم دخوله لدى خادم يثق به ويوافق على ما يُسمح للتطبيق بفعله، ثم يتلقى التطبيق رمز وصول (access token) لا يحمل إلا هذا الإذن. ومثال المواصفة نفسها: خدمة طباعة تقرأ صورك المحفوظة في موقع لمشاركة الصور دون أن تعرف كلمة مرورك هناك أبداً.
لماذا يهم
عيوب OAuth عيوب أمنية. فعنوان إعادة التوجيه (redirect URI) الذي لا يُطابَق بدقة قد يوصل رموز التفويض إلى مهاجم، وغياب فحص state يقبل تسجيل دخول مزوّراً، والنطاق الذي لا تفرضه واجهة API يحوّل صلاحية القراءة إلى صلاحية كتابة، ورمز التحديث الذي لا تنتهي صلاحيته يُبقي هاتفاً مفقوداً مسجَّل الدخول. ويصف RFC 9700، وهو أفضل الممارسات الحالية لأمان OAuth، هذه الهجمات وطرق التصدي لها.
وبالنسبة إلى المختبِر، يعبر تسجيل الدخول الواحد المتصفحَ والعميلَ وخادمَ التفويض وواجهةَ API. ومعرفة الأدوار والرسائل تحوّل بلاغ «تسجيل الدخول لا يعمل» إلى الخطوة الدقيقة التي فشلت والخطأ الذي أعادته.
الأدوار الأربعة وما تتبادله
يعرّف RFC 6749 أربعة أدوار: مالك المورد (Resource Owner) وهو المستخدم عادةً، والعميل (Client) وهو التطبيق الذي يريد الوصول، وخادم التفويض (Authorization Server) الذي يسجّل دخول المستخدم ويطلب موافقته ويُصدر الرموز، وخادم الموارد (Resource Server) وهو واجهة API. والعميل السري (confidential client)، مثل تطبيق ويب له خادم خلفي، يستطيع الاحتفاظ بسرّ؛ أما العميل العام (public client)، مثل تطبيق الصفحة الواحدة أو تطبيق الجوال، فلا يستطيع.
- رمز التفويض (authorization code): قصير العمر ويُستخدم مرة واحدة، ويعود عبر المتصفح؛ ويوصي RFC 6749 بألا يتجاوز عمره 10 دقائق.
- رمز الوصول (access token): يُرسل إلى واجهة API، وغالباً بوصفه رمز حامل (Bearer) كما في درس مصادقة واجهات API. ولا يحدد OAuth صيغته: قد يكون JWT أو سلسلة نصية معتمة (opaque).
- رمز التحديث (refresh token): اختياري وأطول عمراً؛ لا يُرسل إلا إلى خادم التفويض للحصول على رموز وصول جديدة، ولا يذهب أبداً إلى واجهة API.
- النطاق (scope): صلاحيات مثل
appointments.read. وقد يمنح الخادم أقل مما طُلب، وعندها يجب أن يعيد النطاق الممنوح.
بيانات اعتماد العميل والتحديث وانتهاء الصلاحية
عند غياب المستخدم، كمهمة تقارير ليلية، يستخدم العميل السري منحة بيانات اعتماد العميل (Client Credentials): يُثبت هويته ببيانات اعتماده الخاصة ويتلقى رمز وصول لصلاحياته هو، ودون رمز تحديث في العادة.
تكون رموز الوصول قصيرة العمر حتى لا يفيد الرمز المسرَّب إلا لوقت قصير. وعند انتهاء صلاحيته يرسل العميل grant_type=refresh_token، ولا يستطيع الحصول على نطاق لم يوافق عليه المستخدم. وللعملاء العامين يشترط RFC 9700 أن تكون رموز التحديث مقيّدة بالمرسِل (sender-constrained)، أي مربوطة تشفيرياً بنسخة العميل تلك فلا تنفع نسخة مسروقة منها وحدها، أو مُدوَّرة: ومع التدوير (rotation) يعيد كل تحديث رمز تحديث جديداً ويُبطل القديم.
# Basic credentials = Base64 of client_id:client_secret (example values from RFC 6749)
POST /token HTTP/1.1
Host: auth.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=reports.read
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{"access_token":"2YotnFZFEjr1zCsicMWpAA","token_type":"Bearer",
"expires_in":900,"scope":"reports.read"}OpenID Connect باختصار
يخبر OAuth 2.0 العميلَ بما يحق له الوصول إليه، لا بهوية المستخدم. ويضيف OpenID Connect (اختصاراً OIDC) طبقة الهوية: مع النطاق openid تحمل استجابة الرموز أيضاً رمز هوية (ID Token)، وهو JWT موجّه إلى العميل يذكر من سجّل الدخول، بمطالبات مثل iss وsub (معرّف المستخدم لدى تلك الجهة المُصدِرة) وaud (معرّف العميل نفسه client_id) وexp، وnonce إن أرسله العميل.
يتحقق العميل من رمز الهوية كما يشرح دليل JWT ويحتفظ به لنفسه؛ ولا يذهب إلى واجهة API إلا رمز الوصول.
مثال تطبيقي: اختبار تسجيل الدخول في تطبيق حجز
تطبيق لحجز مواعيد عيادة هو عميل عام يستخدم تدفق رمز التفويض مع PKCE وOIDC؛ وتتطلب واجهته البرمجية appointments.read لعرض المواعيد وappointments.write للحجز. أنت تختبر التكامل في بيئة الاختبار بمستخدمي اختبار وبتسجيل العميل الخاص بالاختبار، لا منتجَ مزوّد الهوية نفسه. وتعيد نقطة نهاية الرموز قيمة error في جسم JSON، بينما تعيدها واجهة API في تحدي WWW-Authenticate كما يحدد RFC 6750. ومعظم شروط الاختبار سلبية:
- سجّل الدخول ووافق واعرض المواعيد ←
200، والنطاق الممنوحscopeيساوي ما وافق عليه المستخدم. - عدّل
stateفي عنوان العودة ← يرفضه التطبيق ولا ينشئ جلسة. - وجّه
redirect_uriإلى مضيف آخر ← صفحة خطأ ولا إعادة توجيه. - استخدم رمز التفويض نفسه مرتين، أو أرسل
code_verifierخاطئاً ←400معinvalid_grant. - ارفض الموافقة ← يعالج التطبيق
error=access_deniedبرسالة واضحة. - احجز برمز للقراءة فقط ←
403معinsufficient_scope. - استدعِ واجهة API برمز وصول منتهي الصلاحية أو برمز الهوية ←
401معinvalid_token. - مع تفعيل التدوير، أعد استخدام رمز تحديث قديم ←
400معinvalid_grant، ويتوقع RFC 9700 أن يُبطَل أحدث رمز تحديث أيضاً.
أخطاء شائعة
- معاملة رمز الوصول بوصفه دليلاً على هوية المستخدم. فالهوية تأتي من رمز هوية OIDC جرى التحقق منه.
- اختبار المسار السعيد (happy path) في المتصفح فقط، وعدم استدعاء نقطة نهاية الرموز مباشرةً برمز مُعاد استخدامه أو بقيمة تحقق خاطئة أو بـ
redirect_uriمعدَّل. - التأكد من أن واجهة API تقبل الرموز الصالحة دون التأكد من أنها ترفض الرموز المنتهية، والرموز الصادرة لواجهة أخرى، والرموز التي تفتقد النطاق المطلوب.
- لصق رموز حقيقية أو أسرار عملاء في التذاكر أو المحادثات أو لقطات الشاشة بدلاً من حجبها.
- إجراء اختبارات الأمان على مزوّد هوية خارجي دون إذن مكتوب، بدلاً من اختبار التكامل الخاص بك.
من منظور ضمان الجودة
ابدأ من مراجعة التصميم. اسأل عن كل عميل: أي منحة يستخدم، وهل هو عام أم سري، وهل تُفرض PKCE، وما عناوين إعادة التوجيه المسجّلة، وأي نقطة نهاية تتطلب أي نطاق، وكم تعيش الرموز، وهل تُدوَّر رموز التحديث. الإجابات هي أساس الاختبار (test basis) لديك، والإجابة الغائبة ملاحظةٌ تستحق التسجيل بحد ذاتها.
كثير من النتائج المتوقعة إعدادات لا شيفرة، فاعمل مع المطورين ومع مسؤول خادم التفويض. وسجّل client_id والمنحة والنطاقات المطلوبة والممنوحة ونقطة النهاية والحالة وقيمة error في OAuth ومعرّف الطلب، واحجب كل رمز وكل سر.
تمرين عملي
افحص زوج PKCE واستجابة رموز
في بيئة الاختبار طلب تطبيق الحجز scope=openid appointments.read بزوج PKCE الموضّح أدناه، وتلقى استجابة الرموز التي تحته. تحقّق من الزوج، وراجع الاستجابة، واكتب أربعة شروط اختبار سلبية لنقطة نهاية الرموز.
code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM (method S256)
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "openid appointments.read appointments.write profile"
}- احسب تجزئة
code_verifierفي أداة «مولّد SHA-256» بمخرجات Base64url، وقارن النتيجة بـcode_challenge. - قارن النطاق الممنوح
scopeبالنطاق المطلوب. - تحقّق مما يشترطه OpenID Connect في الاستجابة عند طلب
openid. - اكتب أربعة شروط لنقطة نهاية الرموز مع الحالة المتوقعة وقيمة
error.
إظهار الإجابة النموذجية
التجزئة تساوي قيمة التحدي، فالزوج صالح (وهو المثال الوارد في RFC 7636، الملحق B).
مُنح appointments.write وprofile مع أنهما لم يُطلبا ولم يوافق عليهما المستخدم: عيب في مبدأ أقل الصلاحيات ما لم توجد سياسة موثّقة في الخادم تفسّره.
لم يُعَد id_token رغم طلب openid، فلا يستطيع العميل معرفة من سجّل الدخول؛ ويشترطه OpenID Connect في هذه الاستجابة. أما expires_in (15 دقيقة) وCache-Control: no-store فكما هو متوقع.
يتوقع كل شرط 400 مع invalid_grant ودون أي رموز: استخدام رمز التفويض نفسه مرتين (ثم تحقّق من إبطال الرموز الصادرة في التبادل الأول حيث يدعم الخادم ذلك)؛ وcode_verifier خاطئ؛ وredirect_uri يختلف عن طلب التفويض؛ ورمز منتهي الصلاحية.
أسئلة شائعة
هل OAuth 2.0 بروتوكول للمصادقة؟
لا. OAuth 2.0 إطار لتفويض الوصول نيابةً عن المستخدم (delegated authorization): يتيح للعميل الحصول على رمز وصول لواجهة API، ولا يحدد كيف يعرف العميل هوية المستخدم. ويضيف OpenID Connect ذلك عبر رمز الهوية، ولهذا ينبغي بناء ميزات تسجيل الدخول على OIDC لا على OAuth 2.0 وحده.
لماذا نستخدم PKCE إذا كان لدى العميل سرّ أصلاً؟
السرّ يُثبت هوية العميل، لكنه لا يربط رمز تفويض بعينه بالطلب الذي أنتجه. أما PKCE فتفعل ذلك، فتمنع المهاجم أيضاً من حقن رمز مسروق. ولهذا يشترط RFC 9700 استخدام PKCE للعملاء العامين ويوصي به للعملاء السريين كذلك.
هل ما زالت المنحة الضمنية ومنحة كلمة المرور مقبولتين؟
ينص RFC 9700 على أنه لا ينبغي استخدام المنحة الضمنية (Implicit)، وأنه يجب عدم استخدام منحة بيانات اعتماد مالك المورد (Resource Owner Password Credentials). وإذا كان منتجك ما زال يعتمد على أيٍّ منهما، فسجّل ذلك خطراً واطلب من الفريق خطة للانتقال إلى منحة رمز التفويض مع PKCE.
هل أستطيع التدرّب على هذه الاختبارات مع مزوّد هوية عام؟
فقط في حدود ما يسمح به ذلك المزوّد للمطورين، ودون اختبارات هجومية أو كثيفة الحجم أبداً. نفّذ الاختبارات السلبية واختبارات الأمان في بيئة الاختبار الخاصة بك، بمستخدمي اختبار وتسجيل عميل للاختبار، أو على أنظمة لديك إذن مكتوب باختبارها.
المراجع
- RFC 6749: The OAuth 2.0 Authorization Frameworkأدوار OAuth 2.0 وأنواع المنح ونقاط نهاية الرموز التي تُصدر رموز الوصول والتحديث.
- RFC 9700: Best Current Practice for OAuth 2.0 Securityإرشادات الأمان الحالية لـ OAuth 2.0: استخدام PKCE للعملاء العامين ومطابقة عناوين إعادة التوجيه بدقة وتقييد الرموز بالمرسل.
- RFC 7636: Proof Key for Code Exchange by OAuth Public Clientsآلية PKCE: قيمتا code_verifier وcode_challenge (بطريقة S256) اللتان تربطان رمز التفويض في OAuth بالعميل الذي طلبه، فتُحبطان اعتراض الرمز.
- OpenID Connect Core 1.0 incorporating errata set 2طبقة الهوية فوق OAuth 2.0: رمز الهوية (ID Token) وكيفية التحقق منه، وتدفقات المصادقة، والمطالبات القياسية، ونقطة UserInfo.
تدرّب بأدوات متقن
فكّ ترميز رموز الوصول والهوية في OAuth 2.0 / OpenID Connect وفهم النطاقات والجماهير ومدد الصلاحية.
ولّد استجابات تسجيل دخول ورموز وهمية: رمز الوصول ورمز التحديث وانتهاء الصلاحية والملف الشخصي للمستخدم وأشكال OAuth2/OIDC.
توليد تجزئات SHA-256 للنصوص — المعيار الأساسي لفحوصات السلامة والتوقيعات.
اختبر مصادقة Bearer وBasic ومفتاح API والترويسات المخصّصة على نقطة نهاية وقارن الاستجابات المصرّح بها بغير المصرّح بها.
التحقق مما إذا كان رمز JWT منتهي الصلاحية أو لم يصبح صالحاً بعد، أو المدة المتبقية حتى انتهاء صلاحيته.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.