במדריך הזה מוסבר איך להטמיע 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 להפניה אוטומטית משמש ליצירת הוכחת DPoP. אסימון הרענון קשור להוכחה שכלולה ככותרת HTTP בכל הבקשות הבאות לנקודת הקצה של האסימון.
אפליקציות SPA טהורות בצד הלקוח שלא משתמשות בסודות לא יכולות להשתמש ב-DPoP ישירות בגלל הדרישה client_secret וההגבלות של CORS על הכותרת DPoP-Nonce. כדי לאבטח אפליקציות SPA, צריך לנתב את התנועה דרך Backend-for-Frontend (BFF) שפועל כלקוח סודי, מאפשר access_type=offline ומשתמש ב-DPoP כדי לקשור את שרת טוקן הרענון בצד השרת.
יצירת הוכחת 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 הן ערכים קבועים ולא משתנים כששולחים בקשה לנקודת הקצה של Google לטוקנים.
שתי הטענות האחרות: iat ו-jti חייבות להופיע בכל בקשה. הערך של iat הוא חותמת הזמן של הנפקת הטוקן, והוא משתנה בכל בקשה. הערך של הצהרת מזהה ה-JWT (jti) תלוי בסוג ההמרה. כשמחליפים קוד הרשאה באסימוני גישה ורענון, הערך של jti הוא גיבוב SHA256 בקידוד Base-64 ובקידוד URL של קוד ההרשאה, כמו jti =
BASE64URL(SHA-256(authorization_code)).
זו דוגמה לגוף של מטען ייעודי (payload):
{
"jti": "o29CN8LIY0l_N8iy5-ilon1guad9NFQHFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"iat": 1784822025
}
כותרת ה-JOSE וגוף המטען הייעודי (payload) מקודדים כ-JWT (RFC7519) לשימוש ישיר בכותרת ה-HTTP DPoP בבקשת האסימון:
$ 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"
}
הערך החד-פעמי (nonce) שנוצר על ידי שרת ההרשאות של Google חייב להיכלל בכל בקשה עוקבת לאסימון. חשוב לזכור שערך nonce משמש רק פעם אחת, ואם ערך nonce חסר, לא תקין, לא בתוקף או נעשה בו שימוש חוזר, הוא נדחה עם תגובת HTTP 400. במקרה כזה, מוחזר נונס חדש לשימוש בניסיונות חוזרים.
שינויים בתהליך רענון הטוקן
כדי לעדכן תהליך קיים של רענון טוקן OAuth 2.0, צריך ליצור ולשלוח הוכחת DPoP ככותרת HTTP כשמחליפים טוקן רענון בטוקנים חדשים, כמו שמוצג בשלבים 2-5 באיור 2.
יצירת הוכחת DPoP
השיטה ליצירת הוכחה לרענון האסימון שונה מהשיטה בתרחיש של קוד הרשאה. כותרת ה-JOSE נוצרת באותו אופן שמתואר למעלה לגבי יצירת בקשה לקוד הרשאה. גוף ההוכחה בנוי באופן דומה, אבל הוא כולל הצהרת nonce והשדה jti מכיל מחרוזת אקראית ייחודית.
כדי ליצור את גוף המטען הייעודי (payload), צריך לכלול את הערך של כותרת ה-HTTP DPoP-Nonce שהוחזרה קודם בתביעה nonce, ולעדכן את חותמת הזמן של הנפקת הטוקן (iat) בכל בקשה. מזהה ה-JWT (jti) הוא מחרוזת אקראית ייחודית שנוצרת לכל בקשה באמצעות WebCrypto API המובנה 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) לשימוש ישיר בכותרת ה-HTTP של DPoP בבקשת הטוקן.
ההוכחה מתווספת ככותרת 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 (למשל, כשעוברים מהחלפת קוד ההרשאה הראשונית לבקשת רענון טוקן), השרת של 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."
}
אם הפעולה בוצעה ללא שגיאות, מוחזרים ערך nonce חדש וטוקן גישה לטווח קצר:
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 לאפליקציות אינטרנט ושיטות מומלצות.