مفاهيم 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. وقد يمنح الخادم أقل مما طُلب، وعندها يجب أن يعيد النطاق الممنوح.

جرّبها: مفكّك ترميز رموز OAuth جرّبها: استجابة مصادقة وهمية

تدفق رمز التفويض مع PKCE

المنحة (grant) هي الطريقة التي يحصل بها العميل على الرموز. وعند وجود مستخدم، استخدم منحة رمز التفويض مع PKCE (مفتاح الإثبات لتبادل الرمز، Proof Key for Code Exchange، المعرَّف في RFC 7636). يسجّل المستخدم دخوله لدى خادم التفويض، فلا تصل كلمة المرور إلى العميل أبداً. ثم يعيد الخادم توجيه المتصفح إلى العميل ومعه رمز تفويض؛ وهذا الجزء الذي يمر عبر المتصفح هو القناة الأمامية (front channel). ثم يقدّم العميل الرمز في طلب مباشر إلى نقطة نهاية الرموز (token endpoint) ليستبدله برموز، وهذه هي القناة الخلفية (back channel).

تربط PKCE الرمز بالعميل الذي طلبه. ينشئ العميل قيمة عشوائية اسمها code_verifier (قيمة التحقق)، ولا يرسل إلا تجزئتها (hash) بخوارزمية SHA-256 مرمّزةً بـ Base64url بوصفها code_challenge (قيمة التحدي، بالطريقة S256). والتجزئة بصمة باتجاه واحد: يستطيع أي أحد حسابها من قيمة التحقق، لكن لا يستطيع أحد استنتاج قيمة التحقق منها. وعند استبدال الرمز يقدّم العميل قيمة التحقق نفسها، فيتأكد الخادم من أن تجزئتها تساوي قيمة التحدي، فيصبح الرمز المسروق بلا فائدة من دونها. ويشترط RFC 9700 استخدام PKCE للعملاء العامين ويوصي به للعملاء السريين.

تدفق رمز التفويض مع PKCE: يرسل العميل قيمة تحدٍّ مجزّأة، ويتلقى عبر المتصفح رمزاً صالحاً لمرة واحدة، ثم يقدّمه مع قيمة التحقق السرية مقابل رمز الوصول، ويستدعي به واجهة API.
  • المستخدم والمتصفح مالك المورد
  • تطبيق العميل عميل عام
  • خادم التفويض auth.example.com
  • خادم الموارد واجهة API
  1. 1 تطبيق العميل داخليًا: عند بدء تسجيل الدخول ينشئ state وزوج PKCE قيمة code_challenge هي تجزئة SHA-256 لقيمة code_verifier بترميز Base64url، وتبقى قيمة التحقق لدى العميل
  2. 2 من تطبيق العميل إلى خادم التفويض: طلب التفويض مع client_id وredirect_uri وscope وstate وقيمة التحدي GET /authorize?response_type=code…
  3. 3 من المستخدم والمتصفح إلى خادم التفويض: يسجّل الدخول ويوافق على النطاقات لا تذهب كلمة المرور إلا إلى خادم التفويض
  4. 4 من خادم التفويض إلى تطبيق العميل: إعادة توجيه ومعها رمز صالح لمرة واحدة ردّ إلى redirect_uri المسجّل، ومعه state نفسها 302 /cb?code=…&state=…
  5. 5 تطبيق العميل داخليًا: يطابق state مع الجلسة
  6. 6 من تطبيق العميل إلى خادم التفويض: يقدّم الرمز مع قيمة التحقق يرسل الرمز وredirect_uri نفسه وcode_verifier POST /token
  7. 7 خادم التفويض داخليًا: يتحقق من الرمز ومن زوج PKCE رمز لم يُستخدم ولم تنتهِ صلاحيته، وredirect_uri نفسه، وتجزئة قيمة التحقق تساوي التحدي؛ وإلا فـ invalid_grant
  8. 8 من خادم التفويض إلى تطبيق العميل: رمز الوصول ومدة الصلاحية ورمز تحديث اختياري ردّ ومعها رمز هوية عند طلب openid
  9. 9 من تطبيق العميل إلى خادم الموارد: يستدعي واجهة API برمز الوصول Authorization: Bearer …
  10. 10 خادم الموارد داخليًا: يتحقق من الرمز انتهاء الصلاحية والجمهور (audience) والنطاق الذي تتطلبه نقطة النهاية
  11. 11 من خادم الموارد إلى تطبيق العميل: البيانات، أو الرفض ردّ 200؛ أو 401 مع invalid_token، أو 403 مع insufficient_scope

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

الخطوات من 2 إلى 4 تمرّ عبر المتصفح (القناة الأمامية)، والخطوات من 6 إلى 8 طلب مباشر من العميل (القناة الخلفية). ولا يعرض الشكل مسارات الخطأ.

  • state: قيمة عشوائية مرتبطة بجلسة المتصفح تُفحص عند العودة، فيُرفض أي رد مزوّر إلى عنوان العودة (callback).
  • redirect_uri: يُطابَق تطابقاً تاماً مع قيمة مسجّلة مسبقاً. وعند عدم التطابق يعرض الخادم خطأً ولا يعيد التوجيه.
# Front channel: the browser carries the authorization request (wrapped for reading)
GET /authorize?response_type=code&client_id=booking-app
    &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcb
    &scope=openid%20appointments.read&state=af0ifjsldkj
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256 HTTP/1.1
Host: auth.example.com

# Front channel: after sign-in and consent, the redirect back with the code
HTTP/1.1 302 Found
Location: https://app.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj

# Back channel: the client redeems the code with the verifier (body wrapped for reading)
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&client_id=booking-app
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcb
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

جرّبها: مولّد SHA-256

بيانات اعتماد العميل والتحديث وانتهاء الصلاحية

عند غياب المستخدم، كمهمة تقارير ليلية، يستخدم العميل السري منحة بيانات اعتماد العميل (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 أن يُبطَل أحدث رمز تحديث أيضاً.

جرّبها: مختبِر مصادقة API جرّبها: فاحص انتهاء صلاحية JWT

أخطاء شائعة

  • معاملة رمز الوصول بوصفه دليلاً على هوية المستخدم. فالهوية تأتي من رمز هوية 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"
}
الخطوات
  1. احسب تجزئة code_verifier في أداة «مولّد SHA-256» بمخرجات Base64url، وقارن النتيجة بـ code_challenge.
  2. قارن النطاق الممنوح scope بالنطاق المطلوب.
  3. تحقّق مما يشترطه OpenID Connect في الاستجابة عند طلب openid.
  4. اكتب أربعة شروط لنقطة نهاية الرموز مع الحالة المتوقعة وقيمة 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.

هل أستطيع التدرّب على هذه الاختبارات مع مزوّد هوية عام؟

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

المراجع

تدرّب بأدوات متقن

فكّ ترميز رموز الوصول والهوية في OAuth 2.0 / OpenID Connect وفهم النطاقات والجماهير ومدد الصلاحية.

الأمان

ولّد استجابات تسجيل دخول ورموز وهمية: رمز الوصول ورمز التحديث وانتهاء الصلاحية والملف الشخصي للمستخدم وأشكال OAuth2/OIDC.

المحاكاة

توليد تجزئات SHA-256 للنصوص — المعيار الأساسي لفحوصات السلامة والتوقيعات.

الأمان

اختبر مصادقة Bearer وBasic ومفتاح API والترويسات المخصّصة على نقطة نهاية وقارن الاستجابات المصرّح بها بغير المصرّح بها.

اختبار واجهات API

التحقق مما إذا كان رمز JWT منتهي الصلاحية أو لم يصبح صالحاً بعد، أو المدة المتبقية حتى انتهاء صلاحيته.

الأمان

للتعمّق أكثر

تقدّمك في هذا الموضوع

جزء من مسار تعلم ضمان الجودة