Hướng dẫn sử dụng DPoP

Hướng dẫn này trình bày chi tiết cách triển khai DPoP (Chứng minh quyền sở hữu) trong các hoạt động tích hợp OAuth 2.0 với Nền tảng OAuth của Google. DPoP (được định nghĩa trong RFC 9449) bảo vệ ứng dụng của bạn khỏi hành vi đánh cắp mã thông báo và tấn công phát lại bằng cách liên kết mã thông báo với một cặp khoá bất đối xứng do ứng dụng tạo theo cách mã hoá.

Thay đổi về quy trình sử dụng mã uỷ quyền

Để thêm DPoP vào quy trình sử dụng mã uỷ quyền OAuth 2.0 hiện có, bạn cần tạo và lưu trữ một cặp khoá, tạo JWT bằng chứng DPoP và thêm bằng chứng đó làm tiêu đề HTTP khi mã uỷ quyền được trao đổi lấy mã làm mới, như trong bước 5 và 6 của Hình 1.

Quy trình sử dụng mã uỷ quyền có DPoP
Hình 1. Trình tự các sự kiện trong quy trình sử dụng Mã uỷ quyền có DPoP.

Yêu cầu mã uỷ quyền

Yêu cầu uỷ quyền được tạo theo cách thông thường. Ví dụ:

$ 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"

Mã uỷ quyền được trả về dưới dạng tham số URI chuyển hướng được dùng trong việc tạo bằng chứng DPoP. Mã làm mới được liên kết với bằng chứng có trong tiêu đề HTTP trong tất cả các yêu cầu tiếp theo gửi đến điểm cuối mã thông báo.

Các SPA phía máy khách thuần tuý, không có bí mật không thể sử dụng trực tiếp DPoP do yêu cầu client_secret và các quy định hạn chế về CORS đối với tiêu đề DPoP-Nonce. Để bảo mật SPA, hãy định tuyến lưu lượng truy cập thông qua một Backend-for-Frontend (BFF) đóng vai trò là Ứng dụng khách bí mật, bật access_type=offline và sử dụng DPoP để liên kết mã làm mới phía máy chủ.

Tạo bằng chứng DPoP

Bằng chứng chứa tiêu đề JOSE và tải trọng.

Để tạo tiêu đề, hãy tạo một cặp khoá EC P-256 (ES256) và đưa toạ độ khoá công khai (x và y) vào tham số jwk. Cặp khoá RSA (RS256) cũng được hỗ trợ, mặc dù bạn nên dùng ES256 do kích thước chữ ký nhỏ hơn và chi phí tính toán thấp hơn.

Sau đây là ví dụ về tiêu đề JOSE:

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

Để tạo tải trọng bằng chứng, bạn cần có 4 thông báo xác nhận quyền sở hữu (htm, htu, iat và jti), cùng với một thông báo xác nhận quyền sở hữu nonce không bắt buộc.

Hai thông báo xác nhận quyền sở hữu: htm: POST và htu: https://oauth2.googleapis.com/token là các giá trị cố định và không thay đổi khi đưa ra yêu cầu đến điểm cuối mã thông báo của Google.

Hai thông báo xác nhận quyền sở hữu còn lại: iat và jti phải được tạo cho mọi yêu cầu. Giá trị của iat là dấu thời gian được phát hành và thay đổi theo từng yêu cầu. Giá trị của thông báo xác nhận quyền sở hữu mã JWT (jti) phụ thuộc vào loại giao dịch trao đổi. Khi mã uỷ quyền được đổi lấy mã truy cập và mã làm mới, giá trị của jti là hàm băm SHA256 được mã hoá theo Base-64 và URL của mã uỷ quyền, chẳng hạn như jti = BASE64URL(SHA-256(authorization_code)). Xác nhận quyền sở hữu nonce là không bắt buộc trong lần trao đổi mã uỷ quyền ban đầu và chỉ cần thiết nếu ứng dụng của bạn đã có DPoP-Nonce hợp lệ từ một yêu cầu trước đó.

Sau đây là ví dụ về nội dung tải trọng:

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

Tiêu đề JOSE và nội dung tải trọng được mã hoá và ký dưới dạng JWT (RFC 7519) để sử dụng trực tiếp trong tiêu đề HTTP DPoP trong yêu cầu mã thông báo. Đối với ES256, chữ ký JWS phải sử dụng định dạng IEEE P1363 thô (R || S, 64 byte cho 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"

Mã làm mới liên kết với DPoP được trả về cùng với tiêu đề HTTP DPoP-Nonce, ví dụ:

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"
}

Luôn lưu vào bộ nhớ đệm và gửi DPoP-Nonce gần đây nhất do máy chủ uỷ quyền của Google trả về trong các yêu cầu mã thông báo tiếp theo. Số chỉ dùng một lần sẽ hết hạn định kỳ, và nếu bằng chứng thiếu thông báo xác nhận quyền sở hữu nonce bắt buộc hoặc chứa số chỉ dùng một lần không hợp lệ hoặc đã hết hạn, thì máy chủ sẽ trả về phản hồi HTTP 400 (use_dpop_nonce) với tiêu đề DPoP-Nonce mới để sử dụng trong các lần thử lại.

Thay đổi đối với quy trình làm mới mã thông báo

Để cập nhật quy trình Làm mới mã thông báo OAuth 2.0 hiện có, bạn cần tạo và gửi bằng chứng DPoP dưới dạng tiêu đề HTTP khi mã làm mới được đổi lấy mã thông báo mới, như trong các bước từ 2 đến 5 của Hình 2.

Quy trình làm mới mã thông báo bằng DPoP
Hình 2. Trình tự các sự kiện trong quy trình Làm mới mã thông báo có xử lý lỗi và thử lại.

Tạo bằng chứng DPoP

Phương thức tạo bằng chứng để làm mới mã thông báo khác với trường hợp mã uỷ quyền. Tiêu đề JOSE được tạo theo cách tương tự như mô tả trước đó khi tạo yêu cầu mã uỷ quyền. Nội dung bằng chứng được tạo tương tự nhưng bao gồm một thông báo xác nhận quyền sở hữu nonce và jti chứa một chuỗi ngẫu nhiên duy nhất.

Để tạo nội dung tải trọng, bạn phải đưa giá trị tiêu đề HTTP DPoP-Nonce được trả về trước đó vào thông báo xác nhận quyền sở hữu nonce và cập nhật dấu thời gian phát hành (iat) cho mọi yêu cầu. Mã JWT (jti) là một chuỗi ngẫu nhiên duy nhất được tạo cho mỗi yêu cầu, sử dụng WebCrypto API tích hợp sẵn crypto.getRandomValues(new Uint8Array(24)) và mã hoá chuỗi theo Base64URL.

Sau đây là ví dụ về nội dung tải trọng chứa jti, nonce và iat:

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

Phần tải trọng và tiêu đề JOSE được mã hoá dưới dạng JWT (RFC7519) để sử dụng trực tiếp trong tiêu đề HTTP DPoP trong yêu cầu mã thông báo.

Bằng chứng được thêm dưới dạng tiêu đề DPoP vào yêu cầu làm mới mã thông báo:

$ 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"

Khi một số chỉ dùng một lần đã hết hạn hoặc không hợp lệ được sử dụng hoặc khi chuyển đổi giữa các quy trình công việc OAuth khác nhau (chẳng hạn như chuyển từ yêu cầu Trao đổi mã uỷ quyền ban đầu sang yêu cầu Làm mới mã thông báo), máy chủ của Google sẽ thực thi việc tách biệt quy trình công việc. Điều này có nghĩa là máy chủ từ chối nonce vô điều kiện bằng một thử thách HTTP 400 use_dpop_nonce để thiết lập một không gian tên nonce mới cho quy trình công việc mới.

Sau đây là ví dụ về phản hồi 400 yêu cầu thử lại và tạo bằng chứng mới bằng giá trị 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."
}

Nếu thành công, một số chỉ dùng một lần mới và mã truy cập có thời hạn ngắn sẽ được trả về:

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"
}

Lưu giá trị DPoP-Nonce để sử dụng trong yêu cầu tiếp theo.

Hãy xem bài viết Sử dụng OAuth 2.0 cho ứng dụng máy chủ web và Các phương pháp hay nhất để biết thêm thông tin chi tiết và đề xuất.