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

این راهنما جزئیات نحوه پیاده‌سازی DPoP (اثبات اثبات مالکیت) را در ادغام‌های OAuth 2.0 شما با پلتفرم OAuth گوگل شرح می‌دهد. 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 های سمت کلاینت خالص و بدون مخفی‌کاری، به دلیل الزام client_secret و محدودیت‌های CORS در هدر DPoP-Nonce ، نمی‌توانند مستقیماً از DPoP استفاده کنند. برای ایمن‌سازی SPAها، ترافیک را از طریق یک Backend-for-Frontend (BFF) که به عنوان یک کلاینت محرمانه عمل می‌کند، access_type=offline را فعال می‌کند و از DPoP برای اتصال توکن refresh سمت سرور استفاده می‌کند، مسیریابی کنید.

اثبات DPoP را بسازید

یک اثبات شامل یک سرآیند JOSE و یک payload است.

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

دو ادعای دیگر: iat و jti باید برای هر درخواست ایجاد شوند. مقدار iat مهر زمانی صادر شده در زمان است و به ازای هر درخواست تغییر می‌کند. مقدار ادعای JWT ID ( 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 و بدنه‌ی payload به صورت 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 به همراه یک هدر 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"
}

نانس تولید شده توسط سرور احراز هویت گوگل باید در هر درخواست توکن بعدی گنجانده شود. توجه داشته باشید که مقدار نانس فقط یک بار استفاده می‌شود و مقدار نانس گم شده، نامعتبر، منقضی شده یا دوباره استفاده شده با پاسخ HTTP 400 رد می‌شود. در این حالت، یک نانس جدید برای استفاده در تلاش‌های مجدد بازگردانده می‌شود.

تغییرات جریان به‌روزرسانی توکن

به‌روزرسانی جریان به‌روزرسانی توکن OAuth 2.0 موجود، مستلزم تولید و ارسال یک اثبات DPoP به عنوان یک هدر HTTP است، زمانی که یک توکن به‌روزرسانی با توکن‌های جدید مبادله می‌شود، همانطور که در مراحل ۲ تا ۵ شکل ۲ نشان داده شده است.

جریان به‌روزرسانی توکن با DPoP
شکل ۲. توالی رویدادها در جریان به‌روزرسانی توکن به همراه مدیریت خطا و تلاش مجدد.

ساخت اثبات DPoP

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

برای ساخت بدنه‌ی payload، مقدار هدر HTTP که قبلاً DPoP-Nonce برگردانده شده است، باید در nonce claim گنجانده شود و برچسب زمانی صادر شده ( iat ) برای هر درخواست به‌روزرسانی شود. شناسه‌ی JWT ( jti ) یک رشته‌ی تصادفی منحصر به فرد است که برای هر درخواست تولید می‌شود و از API داخلی WebCrypto به نام crypto.getRandomValues(new Uint8Array(24)) و کدگذاری رشته با Base64URL استفاده می‌کند.

این یک نمونه از بدنه‌ی payload است که شامل jti ، nonce و iat می‌شود:

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

هدر JOSE و بدنه‌ی payload به صورت یک 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"

وقتی از یک nonce منقضی شده، نادرست یا دوباره استفاده شده استفاده می‌شود یا هنگام انتقال بین گردش‌های کاری مختلف OAuth (مانند انتقال از درخواست اولیه Authorization Code Exchange به درخواست Token Refresh)، سرور گوگل جداسازی گردش کار را اعمال می‌کند. این بدان معناست که سرور بدون قید و شرط 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",
  "challenge": "AO4t07IXuovyCbtLEr6VVFZQ_Kb78MMOXTt6-CyZpsJeF62HZ3P_EW55XbWqYcU76Jg"
}

مقدار DPoP-Nonce را برای استفاده در درخواست بعدی ذخیره کنید.

برای جزئیات و توصیه‌های بیشتر، به بخش «استفاده از OAuth 2.0 برای برنامه‌های وب سرور و بهترین شیوه‌ها» مراجعه کنید.