يشرح هذا الدليل بالتفصيل كيفية تنفيذ بروتوكول DPoP (إثبات ملكية الرمز المميز) في عمليات دمج بروتوكول OAuth 2.0 مع منصة OAuth من Google. يحمي بروتوكول DPoP (المحدّد في RFC 9449) تطبيقاتك من سرقة الرموز المميزة وهجمات إعادة التشغيل من خلال ربط الرموز المميزة بشكل مشفّر بزوج مفاتيح غير متماثل من إنشاء العميل.
التغييرات في مسار رمز التفويض
تتطلّب إضافة بروتوكول DPoP إلى مسار رمز تفويض OAuth 2.0 حالي إنشاء زوج مفاتيح وتخزينه، وإنشاء رمز JWT لإثبات بروتوكول DPoP، وتضمين الإثبات كعنوان HTTP عند استبدال رمز التفويض برمز مميز لإعادة التحميل كما هو موضّح في الخطوتَين 5 و6 من الشكل 1.
طلب رمز التفويض
يتم إنشاء طلب التفويض بشكل عادي. على سبيل المثال:
$ 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 لإعادة التوجيه في إنشاء رمز JWT لإثبات بروتوكول DPoP. يرتبط الرمز المميز لإعادة التحميل بالإثبات المضمّن كعنوان HTTP في جميع الطلبات اللاحقة إلى نقطة نهاية الرمز المميز.
لا يمكن لتطبيقات الويب التقدّمية من جهة العميل التي لا تتضمّن معلومات سرية استخدام بروتوكول DPoP مباشرةً بسبب متطلبات client_secret وقيود CORS على عنوان DPoP-Nonce. لتأمين تطبيقات الويب التقدّمية، يجب توجيه حركة البيانات من خلال خادم وسيط بين الواجهة الأمامية والخلفية (BFF) يعمل كعميل سرّي، ويُفعِّل access_type=offline، ويستخدم بروتوكول DPoP لربط الرمز المميز لإعادة التحميل من جهة الخادم.
إنشاء رمز JWT لإثبات بروتوكول DPoP
يحتوي رمز الإثبات على عنوان JOSE وحمولة.
لإنشاء العنوان، يجب إنشاء زوج مفاتيح EC P-256 (ES256) وتضمين إحداثيات المفتاح العام (x وy) في المَعلمة jwk. يمكن أيضًا استخدام زوج مفاتيح RSA، ولكن لا يُنصح بذلك بسبب ارتفاع التكاليف الحسابية.
في ما يلي مثال على عنوان JOSE:
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "VC91y9ZYdfSWaDv8JaI6gx5ifOw2rn3YdqkAB51Uu6E",
"y": "ikPjOtea4k7fWPVrRYwaA4Ww6iVY3pOOICotHwwGV3o"
}
}
لإنشاء حمولة الإثبات، يجب توفير أربع قيم.
إنّ المطالبتَين 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)).
في ما يلي مثال على نص الحمولة:
{
"jti": "o29CN8LIY0l_N8iy5-ilon1guad9NFQHFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"iat": 1784822025
}
يتم ترميز عنوان JOSE ونص الحمولة كرمز JWT (RFC7519) لاستخدامهما مباشرةً في عنوان DPoP HTTP في طلب الرمز المميز:
$ curl -X POST https://oauth2.googleapis.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6\
IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVkM5MXk5WllkZlNXYUR2OEphSTZneDVpZ\
k93MnJuM1lkcWtBQjUxVXU2RSIsInkiOiJpa1BqT3RlYTRrN2ZXUFZyUll3YUE0V3\
c2aVZZM3BPT0lDb3RId3dHVjNvIn19.eyJqdGkiOiJvMjlDTjhMSVkwbF9OOGl5NS\
1pbG9uMWd1YWQ5TkZRSEZPZFhUenJCTmNrIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh\
0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwiaWF0IjoxNzg0ODIy\
MDI1fQ.OSdQCmqTng_uZmGK5UXf8hcEMtoOu7ucmYtl5mx4901RXnj6fJRJQmIeTq\
fhprRBTG_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 مع عنوان DPoP-Nonce HTTP، على سبيل المثال:
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"
}
يجب تضمين الرقم العشوائي الذي أنشأه خادم تفويض Google في كل طلب لاحق للرمز المميز. يُرجى العِلم أنّه يتم استخدام قيمة الرقم العشوائي مرة واحدة فقط، ويتم رفض قيمة الرقم العشوائي المفقودة أو غير الصالحة أو المنتهية الصلاحية أو المعاد استخدامها مع استجابة HTTP 400. في هذه الحالة، يتم عرض رقم عشوائي جديد لاستخدامه في عمليات إعادة المحاولة.
التغييرات في مسار إعادة تحميل الرمز المميز
يتطلّب تعديل مسار إعادة تحميل رمز OAuth 2.0 حالي إنشاء رمز JWT لإثبات بروتوكول DPoP وإرساله كعنوان HTTP عند استبدال رمز مميز لإعادة التحميل برموز مميزة جديدة كما هو موضّح في الخطوات من 2 إلى 5 من الشكل 2.
إنشاء رمز JWT لإثبات بروتوكول DPoP
تختلف طريقة إنشاء رمز الإثبات لإعادة تحميل الرمز المميز عن سيناريو رمز التفويض. يتم إنشاء عنوان JOSE بالطريقة نفسها الموضّحة سابقًا عند إنشاء طلب رمز التفويض. يتم إنشاء نص الإثبات بطريقة مماثلة، ولكنّه يتضمّن مطالبة nonce ويحتوي jti على سلسلة عشوائية فريدة.
لإنشاء نص الحمولة، يجب تضمين قيمة عنوان DPoP-Nonce HTTP الذي تم عرضه سابقًا في مطالبة nonce وتعديل الطابع الزمني للوقت الذي تم فيه إصدار الرمز المميز (iat) لكل طلب. معرّف JWT (jti) هو سلسلة عشوائية فريدة يتم إنشاؤها لكل طلب باستخدام واجهة برمجة تطبيقات WebCrypto المضمّنة crypto.getRandomValues(new Uint8Array(24))، ويتم ترميز السلسلة بترميز Base64URL.
في ما يلي مثال على نص الحمولة الذي يحتوي على jti وnonce وiat:
{
"jti": "o29CN8ZIY0l_K8iy5-ilon1gwad9NF6HFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"nonce": "AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI",
"iat": 1784822025
}
يتم ترميز عنوان JOSE ونص الحمولة كرمز JWT (RFC7519) لاستخدامهما مباشرةً في عنوان DPoP HTTP في طلب الرمز المميز.
تتم إضافة رمز الإثبات كعنوان DPoP إلى طلب إعادة تحميل الرمز المميز:
$ curl -X POST https://oauth2.googleapis.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6\
IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVkM5MXk5WllkZlNXYUR2OEphSTZneDVpZ\
k93MnJuM1lkcWtBQjUxVXU2RSIsInkiOiJpa1BqT3RlYTRrN2ZXUFZyUll3YUE0V3\
c2aVZZM3BPT0lDb3RId3dHVjNvIn19.eyJqdGkiOiJvMjlDTjhaSVkwbF9LOGl5NS\
1pbG9uMWd1YWQ5TkY2SEZPZFhUenJCTmNrIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh\
0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwibm9uY2UiOiJBTjNY\
d0pqWnNqbmIwWnVXa1JsZWs4UVU3d1ktWmhmLTVJUDZ0TzB0T1J6MEtndERUMUJvO\
EZYLXc0bnozcjVsbmVwSSIsImlhdCI6MTc4NDgyMjAyNX0.MEQCIDm09AXo2c9sov\
GrTUkrbEB_k9mra_Dkji-CQ9mSZVP1AiBxbiqkCE7Dt9RKyUT_3kj7q1vCvVggwnW\
JNX3P3vO1mw" \
--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 عزل مسار العمل. يعني ذلك أنّ الخادم يرفض الرقم الخاص بشكل غير مشروط باستخدام تحدّي use_dpop_nonce مع استجابة HTTP 400 لإنشاء مساحة اسم جديدة للرقم الخاص لمسار العمل الجديد.
في ما يلي مثال على استجابة 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",
"challenge": "AO4t07IXuovyCbtLEr6VVFZQ_Kb78MMOXTt6-CyZpsJeF62HZ3P_EW55XbWqYcU76Jg"
}
يجب حفظ قيمة DPoP-Nonce لاستخدامها في الطلب التالي.
يُرجى الاطّلاع على استخدام OAuth 2.0 لتطبيقات خادم الويب و أفضل الممارسات للحصول على مزيد من التفاصيل والاقتراحات.