دليل استخدام DPoP

يوضّح هذا الدليل بالتفصيل كيفية تنفيذ DPoP (إثبات الحيازة) في عمليات دمج OAuth 2.0 مع منصة OAuth من Google. يؤمّن بروتوكول DPoP (المحدّد في RFC 9449) تطبيقاتك ضد سرقة الرموز المميزة وهجمات إعادة التشغيل من خلال ربط الرموز المميزة بشكل مشفَّر بزوج مفاتيح غير متماثل من إنشاء العميل.

تغييرات في مسار رمز التفويض

لإضافة DPoP إلى مسار رمز التفويض الحالي في OAuth 2.0، يجب إنشاء زوج مفاتيح وتخزينه، وإنشاء رمز JWT لإثبات DPoP، وتضمين الإثبات كعنوان HTTP عند استبدال رمز التفويض برمز مميز لإعادة التحميل كما هو موضّح في الخطوتَين 5 و6 من الشكل 1.

مسار رمز التفويض باستخدام DPoP
الشكل 1. تسلسل الأحداث في مسار رمز التفويض باستخدام DPoP

طلب رمز التفويض

يتم إنشاء طلب التفويض بشكل طبيعي. على سبيل المثال:

$ curl -G "https://accounts.google.com/o/oauth2/v2/auth" \
  --data-urlencode "client_id=YOUR_CLIENT_ID.apps.googleusercontent.com" \
  --data-urlencode "redirect_uri=http://127.0.0.1:8080" \
  --data-urlencode "response_type=code" \
  --data-urlencode "scope=calendar.readonly" \
  --data-urlencode "state=AI1Bvapj7E5SDmtW4gohcA" \
  --data-urlencode "code_challenge=PO4pPROl-31Wy9fVZ7uTW9Ga6CrjrSKsf4AAtx_JNM8" \
  --data-urlencode "code_challenge_method=S256" \
  --data-urlencode "nonce=PrMfmSNAvJFPQ7GnlEKUaw" \
  --data-urlencode "access_type=offline" \
  --data-urlencode "prompt=consent"

يتم استخدام رمز التفويض الذي تم عرضه كمَعلمة لمعرّف الموارد المنتظم (URI) الخاص بإعادة التوجيه في إنشاء إثبات DPoP. يرتبط الرمز المميز لإعادة التحميل بالإثبات المضمّن كعنوان HTTP في جميع الطلبات الإضافية إلى نقطة نهاية الرمز المميز.

لا يمكن لتطبيقات الصفحة الواحدة (SPA) من جهة العميل التي لا تتطلّب مصادقة استخدام DPoP مباشرةً بسبب متطلبات client_secret والقيود المفروضة على مشاركة الموارد المتعددة المنشأ (CORS) في عنوان DPoP-Nonce. لتأمين تطبيقات الصفحة الواحدة، يجب توجيه الزيارات من خلال واجهة خلفية للواجهة الأمامية (BFF) تعمل كعميل سري، وتفعّل access_type=offline، وتستخدم DPoP لربط الرمز المميز لإعادة التحميل من جهة الخادم.

إنشاء إثبات DPoP

يحتوي الإثبات على عنوان JOSE وحمولة.

لإنشاء العنوان، أنشئ زوج مفاتيح EC P-256 (ES256) وأدرِج إحداثيات المفتاح العام (x وy) في المَعلمة jwk. يمكن أيضًا استخدام زوج مفاتيح RSA‏ (RS256)، ولكن يُنصح باستخدام ES256 بسبب حجم التوقيع الأصغر وتكلفة الحوسبة الأقل.

في ما يلي مثال على عنوان JOSE:

{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "VC91y9ZYdfSWaDv8JaI6gx5ifOw2rn3YdqkAB51Uu6E",
    "y": "ikPjOtea4k7fWPVrRYwaA4Ww6iVY3pOOICotHwwGV3o"
  }
}

لإنشاء حمولة الإثبات، يجب توفير أربع مطالبات (htm وhtu وiat وjti)، بالإضافة إلى مطالبة nonce اختيارية.

هناك مطلبان: htm: POST وhtu: https://oauth2.googleapis.com/token هما قيمتان ثابتتان ولا تتغيّران عند تقديم طلب إلى نقطة نهاية الرمز المميّز في Google.

يجب إنشاء المطالبتين الأخريين: iat وjti لكل طلب. قيمة iat هي الطابع الزمني لوقت الإصدار وتتغيّر لكل طلب. تعتمد قيمة مطالبة معرّف JWT (jti) على نوع عملية التبادل. عند تبادل رمز التفويض برموز الدخول والرموز المميزة لإعادة التحميل، تكون قيمة jti هي تجزئة SHA256 لرمز التفويض بترميز Base-64 وUrl، مثل jti = BASE64URL(SHA-256(authorization_code)). يكون طلب nonce اختياريًا في عملية تبادل رمز التفويض الأولية، ولا يكون مطلوبًا إلا إذا كان العميل يملك DPoP-Nonce صالحًا من طلب سابق.

في ما يلي مثال على نص الحمولة:

{
  "jti": "o29CN8LIY0l_N8iy5-ilon1guad9NFQHFOdXTzrBNck",
  "htm": "POST",
  "htu": "https://oauth2.googleapis.com/token",
  "iat": 1784822025
}

يتم ترميز عنوان JOSE ونص الحمولة وتوقيعهما كرمز JWT‏ (RFC 7519) للاستخدام المباشر في عنوان HTTP‏ DPoP في طلب الرمز المميّز. بالنسبة إلى ES256، يجب أن يستخدم توقيع JWS تنسيق IEEE P1363 الأولي (R || S، 64 بايت لـ P-256):

$ curl -X POST https://oauth2.googleapis.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVkM5MXk5WllkZlNXYUR2OEphSTZneDVpZk93MnJuM1lkcWtBQjUxVXU2RSIsInkiOiJpa1BqT3RlYTRrN2ZXUFZyUll3YUE0V3c2aVZZM3BPT0lDb3RId3dHVjNvIn19.eyJqdGkiOiJvMjlDTjhMSVkwbF9OOGl5NS1pbG9uMWd1YWQ5TkZRSEZPZFhUenJCTmNrIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwiaWF0IjoxNzg0ODIyMDI1fQ.OSdQCmqTng_uZmGK5UXf8hcEMtoOu7ucmYtl5mx4901RXnj6fJRJQmIeTqfhprRBTG_RSJv2fPcWDqvQbDW7YA" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=4/0AXEQxIDNpLD-qpSIvjHb2Hl10uS_2sk2GBRpO8UJQ78YZF3hZ9LB9kTA1xYLD4xisi4C5w" \
  --data-urlencode "redirect_uri=http://127.0.0.1:8080" \
  --data-urlencode "client_id=YOUR_CLIENT_ID.apps.googleusercontent.com" \
  --data-urlencode "client_secret=YOUR_CLIENT_SECRET" \
  --data-urlencode "code_verifier=q8ZztyVv7HH8E2M-SEL8WaB-7CPs68rejN5UZ9OdYgo"

يتم عرض رمز مميز لإعادة التحميل مرتبط ببروتوكول DPoP مع عنوان HTTP‏ DPoP-Nonce، على سبيل المثال:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DPoP-Nonce: AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI

{
  "access_token": "ya29.a0ARGnu0aebRL97B91dmvm14gTug5wpItFf9MVWq12Hja6yv09A_qxa4T73_z2gFbf32qR4RXispQ7vnOzv6gn0APLQrF51LVa6AOqCVPH2Tupocv8y0JHu4ByEbvgXEEhiHEU8Xa9_w3i-PKBPsKWiLi210RCZdqJjLXkcRrGnoPPjbGPzOPtm6KCJjPrNHG16caOWecaCgYKASESARASFQHGX2MiBn7ihbbk_n-buCbOfl2TDA0206",
  "expires_in": 3599,
  "refresh_token": "1//06dUPZ9FIBQm3CgYIARAAGAYSNwF-L9IrJwuIEKUA_zbBPU-xoCDGM0QrDu7-jv7cMQZ0kARPUK9WhwfFFfbOVEgXDQKmFh4w9GM",
  "scope": "https://www.googleapis.com/auth/calendar.readonly",
  "token_type": "Bearer"
}

يجب دائمًا تخزين أحدث DPoP-Nonce يعرضه خادم التفويض في Google مؤقتًا وإرساله في طلبات الرموز المميزة اللاحقة. تنتهي صلاحية الأرقام الخاصة بشكل دوري، وإذا كان الإثبات يفتقد إلى مطالبة nonce مطلوبة أو يحتوي على رقم خاص غير صالح أو منتهي الصلاحية، يعرض الخادم استجابة HTTP 400 (use_dpop_nonce) مع رأس DPoP-Nonce جديد لاستخدامه في عمليات إعادة المحاولة.

تغييرات في عملية إعادة تحميل الرمز المميّز

يتطلّب تعديل مسار حالي لإعادة تحميل الرمز المميّز لبروتوكول OAuth 2.0 إنشاء وإرسال إثبات DPoP كعنوان HTTP عند استبدال رمز مميّز لإعادة التحميل برموز مميّزة جديدة كما هو موضّح في الخطوات من 2 إلى 5 في الشكل 2.

مسار إعادة تحميل الرمز المميّز باستخدام DPoP
الشكل 2. تسلسل الأحداث في عملية "تجديد الرمز المميّز" مع معالجة الأخطاء وإعادة المحاولة

إنشاء إثبات DPoP

تختلف طريقة إنشاء إثبات لتحديث الرمز المميّز عن سيناريو رمز التفويض. يتم إنشاء عنوان JOSE بالطريقة نفسها الموضّحة سابقًا عند إنشاء طلب رمز التفويض. يتم إنشاء نص الإثبات بشكل مشابه، ولكنه يتضمن مطالبة nonce ويحتوي jti على سلسلة عشوائية فريدة.

لإنشاء نص الحمولة، يجب تضمين قيمة عنوان HTTP DPoP-Nonce التي تم عرضها سابقًا في مطالبة nonce وتعديل الطابع الزمني لوقت الإصدار (iat) لكل طلب. معرّف JWT ‏ (jti) هو سلسلة عشوائية فريدة يتم إنشاؤها لكل طلب باستخدام واجهة برمجة التطبيقات WebCrypto المضمّنة crypto.getRandomValues(new Uint8Array(24)) وترميز السلسلة باستخدام Base64URL.

في ما يلي مثال على نص حمولة يتضمّن jti وnonce وiat:

{
  "jti": "o29CN8ZIY0l_K8iy5-ilon1guad9NF6HFOdXTzrBNck",
  "htm": "POST",
  "htu": "https://oauth2.googleapis.com/token",
  "nonce": "AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI",
  "iat": 1784822025
}

يتم ترميز عنوان JOSE ونص الحمولة كرمز JWT ‏ (RFC7519) للاستخدام المباشر في عنوان HTTP لبروتوكول DPoP في طلب الرمز المميّز.

تتم إضافة الإثبات كعنوان DPoP إلى طلب إعادة تحميل الرمز المميّز:

$ curl -X POST https://oauth2.googleapis.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVkM5MXk5WllkZlNXYUR2OEphSTZneDVpZk93MnJuM1lkcWtBQjUxVXU2RSIsInkiOiJpa1BqT3RlYTRrN2ZXUFZyUll3YUE0V3c2aVZZM3BPT0lDb3RId3dHVjNvIn19.eyJqdGkiOiJvMjlDTjhaSVkwbF9LOGl5NS1pbG9uMWd1YWQ5TkY2SEZPZFhUenJCTmNrIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwibm9uY2UiOiJBTjNYd0pqWnNqbmIwWnVXa1JsZWs4UVU3d1ktWmhmLTVJUDZ0TzB0T1J6MEtndERUMUJvOEZYLXc0bnozcjVsbmVwSSIsImlhdCI6MTc4NDgyMjAyNX0.ObT0BejZz2yi8atNSStsQH-T2atr8OSOL4JD2ZJlU_VxbiqkCE7Dt9RKyUT_3kj7q1vCvVggwnWJNX3P3vO1mw" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=1//06dUPZ9FIBQm3CgYIARAAGAYSNwF-L9IrJwuIEKUA_zbBPU-xoCDGM0QrDu7-jv7cMQZ0kARPUK9WhwfFFfbOVEgXDQKmFh4w9GM" \
  --data-urlencode "client_id=YOUR_CLIENT_ID.apps.googleusercontent.com" \
  --data-urlencode "client_secret=YOUR_CLIENT_SECRET"

عند استخدام رقم خاص منتهي الصلاحية أو غير صالح أو عند الانتقال بين سير عمل OAuth المختلفة (مثل الانتقال من تبادل رمز التفويض الأولي إلى طلب إعادة تحميل الرمز المميّز)، يفرض خادم Google عزل سير العمل. وهذا يعني أنّ الخادم يرفض الرقم الخاص بشكل غير مشروط من خلال طلب HTTP 400 use_dpop_nonce لإنشاء مساحة اسم جديدة للرقم الخاص لسير العمل الجديد.

في ما يلي مثال على استجابة 400 تتطلّب إعادة المحاولة وإنشاء إثبات جديد باستخدام القيمة DPoP-Nonce:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
DPoP-Nonce: AO4t07Kf85RJXmltUhiAiELLPPrJ4zOi66zWxU1uDZbhRcahFBYvT0WlcjSSXULXknSA

{
  "error": "use_dpop_nonce",
  "error_description": "New DPoP nonce issued due to invalid or expired challenge."
}

في حال نجاح العملية، يتم عرض رقم خاص جديد ورمز الدخول قصير الأجل:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DPoP-Nonce: AO4t07IXuovyCbtLEr6VVFZQ_Kb78MMOXTt6-CyZpsJeF62HZ3P_EW55XbWqYcU76Jg=

{
  "access_token": "ya29.a0ARGnu0bDj9BAQYVbF5hi3vw-brBUZBZu1bnInk1hS7gueqEb6QPqUjDGb0MMj9A0QX5FRrJo3FDw-DEDtvVbRUdeCgjwsL_LVVFXz-p-MUyiFyRoufI4KC0Go9aq5cEjD_BWvOJLMSIY6_EnwnhqDgk0XxvzaaAxDnv8PXJAGev_UotcfApstqi0NCxbfi-6Kgull9QaCgYKAUQSARASFQHGX2MiZpMjRS6z4S0RjOkNxn2o1Q0206",
  "expires_in": 3599,
  "scope": "https://www.googleapis.com/auth/calendar.readonly",
  "token_type": "Bearer"
}

احفظ قيمة DPoP-Nonce لاستخدامها في الطلب التالي.

يُرجى الاطّلاع على استخدام OAuth 2.0 لتطبيقات خادم الويب وأفضل الممارسات للحصول على تفاصيل واقتراحات إضافية.