راهنمای پذیرش DPoP

این راهنما جزئیات نحوه پیاده‌سازی DPoP (اثبات مالکیت) در ادغام‌های OAuth 2.0 با «پلاتفرم OAuth» در Google را شرح می‌دهد. DPoP (تعریف‌شده در RFC 9449) برنامه‌های شما را دربرابر سرقت کد و حملات تکرار با اتصال رمزنگاری‌شده کدها به جفت کلید نامتقارن تولیدشده توسط کارخواه ایمن می‌کند.

تغییرات جریان «کد مجوز»

افزودن DPoP به جریان «کد مجوز» OAuth 2.0 موجود مستلزم تولید و ذخیره جفت‌کلید، ساخت JWT اثبات DPoP، و گنجاندن اثبات به‌عنوان سرایند HTTP هنگام مبادله کد مجوز با ژتون بازآوری است، همان‌طور که در مراحل ۵ و ۶ شکل ۱ نشان داده شده است.

جریان کد مجوز با DPoP
شکل ۱. توالی رویدادها در جریان «کد مجوز» بااستفاده از 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،‏ ۶۴ بایت برای 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 است، زمانی که ژتون بازآوری با ژتون‌های جدید مبادله می‌شود، همان‌طور که در مراحل ۲ تا ۵ شکل ۲ نشان داده شده است.

جریان بازآوری کردن توکن با DPoP
شکل ۲. توالی رویدادها در جریان «بازآوری کردن کد» با رسیدگی به خطا و امتحان مجدد.

درحال ساختن اثبات DPoP

روش ساختن اثبات برای بازآوری کردن توکن با سناریو کد مجوز متفاوت است. سرصفحه JOSE به همان روشی که قبلاً هنگام ساختن درخواست کد مجوز توضیح داده شد، ساخته می‌شود. بدنه اثبات به‌طور مشابه ساخته می‌شود اما شامل ادعای nonce است و jti حاوی رشته تصادفی منحصربه‌فردی است.

برای ساختن بدنه بار داده، مقدار سرایند HTTP DPoP-Nonce که قبلاً برگردانده شده است باید در ادعای nonce گنجانده شود و مُهر زمان صادرشده (iat) برای هر درخواست به‌روز شود. شناسه JWT‏ (jti) رشته تصادفی منحصربه‌فردی است که برای هر درخواست بااستفاده از WebCrypto API داخلی 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) کدبندی می‌شوند تا مستقیماً در سرصفحه DPoP HTTP در درخواست کد استفاده شوند.

مدرک به‌عنوان سرصفحه 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"

وقتی از یک nonce منقضی‌شده یا نامعتبر استفاده می‌شود یا هنگام انتقال بین گردش‌های کاری مختلف OAuth (مانند انتقال از «تبادل کد مجوز» اولیه به درخواست «بازآوری کردن توکن»)، سرور Google جداسازی گردش کار را اعمال می‌کند. این یعنی سرور بدون قیدوشرط nonce را با چالش HTTP 400 use_dpop_nonce رد می‌کند تا فضای نام nonce جدیدی برای گردش کار جدید ایجاد کند.

این نمونه‌ای از پاسخ ۴۰۰ است که به تکرار و ساختن اثبات جدید بااستفاده از مقدار 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 برای برنامه‌های کارساز وب و روال‌های مطلوب را ببینید.