Panduan Penerapan DPoP

Panduan ini menjelaskan cara menerapkan DPoP (Demonstrating Proof-of-Possession) dalam integrasi OAuth 2.0 Anda dengan Platform OAuth Google. DPoP (ditentukan dalam RFC 9449) mengamankan aplikasi Anda dari pencurian token dan serangan replay dengan mengikat token secara kriptografis ke pasangan kunci asimetris yang dihasilkan klien.

Perubahan alur Kode Otorisasi

Menambahkan DPoP ke alur Kode Otorisasi OAuth 2.0 yang ada memerlukan pembuatan dan penyimpanan keypair, pembuatan JWT bukti DPoP, dan penyertaan bukti sebagai header HTTP saat kode otorisasi ditukar dengan token refresh seperti yang ditunjukkan pada langkah 5 dan 6 Gambar 1.

Alur Kode Otorisasi dengan DPoP
Gambar 1. Urutan peristiwa dalam alur Kode Otorisasi menggunakan DPoP.

Permintaan kode otorisasi

Permintaan otorisasi dibuat secara normal. Contoh:

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

Kode otorisasi yang ditampilkan sebagai parameter URI pengalihan digunakan dalam pembuatan bukti DPoP. Token refresh terikat ke bukti yang disertakan sebagai header HTTP dalam semua permintaan lebih lanjut ke endpoint token.

SPA sisi klien murni tanpa rahasia tidak dapat menggunakan DPoP secara langsung karena persyaratan client_secret dan batasan CORS pada header DPoP-Nonce. Untuk mengamankan SPA, rutekan traffic melalui Backend-for-Frontend (BFF) yang bertindak sebagai Klien Rahasia, mengaktifkan access_type=offline, dan menggunakan DPoP untuk mengikat token refresh di sisi server.

Membuat bukti DPoP

Bukti berisi header JOSE dan payload.

Untuk membuat header, buat pasangan kunci EC P-256 (ES256) dan sertakan koordinat kunci publik (x dan y) dalam parameter jwk. Keypair RSA (RS256) juga didukung, meskipun ES256 direkomendasikan karena ukuran tanda tangan yang lebih kecil dan biaya komputasi yang lebih rendah.

Berikut adalah contoh header JOSE:

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

Untuk membuat payload bukti, diperlukan empat klaim (htm, htu, iat, dan jti), ditambah klaim nonce opsional.

Dua klaim: htm: POST dan htu: https://oauth2.googleapis.com/token adalah nilai tetap dan tidak berubah saat membuat permintaan ke endpoint token Google.

Dua klaim lainnya: iat dan jti harus dibuat untuk setiap permintaan. Nilai iat adalah stempel waktu yang dikeluarkan dan berubah per permintaan. Nilai klaim ID JWT (jti) bergantung pada jenis pertukaran. Saat kode otorisasi ditukar dengan token akses dan token refresh, nilai jti adalah hash SHA256 berenkode URL dan Base-64 dari kode otorisasi, seperti jti = BASE64URL(SHA-256(authorization_code)). Klaim nonce bersifat opsional pada pertukaran kode otorisasi awal dan hanya diperlukan jika klien Anda sudah memiliki DPoP-Nonce yang valid dari permintaan sebelumnya.

Berikut adalah contoh isi payload:

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

Header JOSE dan isi payload dienkode dan ditandatangani sebagai JWT (RFC 7519) untuk penggunaan langsung di header HTTP DPoP dalam permintaan token. Untuk ES256, tanda tangan JWS harus menggunakan format IEEE P1363 mentah (R || S, 64 byte untuk 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"

Token refresh terikat DPoP ditampilkan bersama dengan header HTTP DPoP-Nonce, misalnya:

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

Selalu simpan dalam cache dan kirim DPoP-Nonce terbaru yang ditampilkan oleh server otorisasi Google dalam permintaan token berikutnya. Nonce akan habis masa berlakunya secara berkala, dan jika bukti tidak memiliki klaim nonce yang diperlukan atau berisi nonce yang tidak valid atau sudah tidak berlaku, server akan menampilkan respons HTTP 400 (use_dpop_nonce) dengan header DPoP-Nonce baru untuk digunakan dalam percobaan ulang.

Perubahan alur pembaruan token

Memperbarui alur Refresh Token OAuth 2.0 yang ada memerlukan pembuatan dan pengiriman bukti DPoP sebagai header HTTP saat token refresh ditukar dengan token baru seperti yang ditunjukkan pada langkah 2-5 di Gambar 2.

Alur Refresh Token dengan DPoP
Gambar 2. Urutan peristiwa dalam alur Token Refresh dengan penanganan error dan percobaan ulang.

Membuat bukti DPoP

Metode untuk membuat bukti pembaruan token berbeda dengan skenario kode otorisasi. Header JOSE dibuat dengan cara yang sama seperti yang dijelaskan sebelumnya saat membuat permintaan kode otorisasi. Isi bukti dibuat dengan cara yang sama, tetapi menyertakan klaim nonce dan jti berisi string acak yang unik.

Untuk membuat isi payload, nilai header HTTP DPoP-Nonce yang ditampilkan sebelumnya harus disertakan dalam klaim nonce dan stempel waktu yang dikeluarkan (iat) diperbarui untuk setiap permintaan. ID JWT (jti) adalah string acak unik yang dihasilkan per permintaan, menggunakan WebCrypto API bawaan crypto.getRandomValues(new Uint8Array(24)) dan string berenkode Base64URL.

Berikut adalah contoh isi payload yang berisi jti, nonce, dan iat:

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

Header JOSE dan isi payload dienkode sebagai JWT (RFC7519) untuk penggunaan langsung di header HTTP DPoP dalam permintaan token.

Bukti ditambahkan sebagai header DPoP ke permintaan refresh token:

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

Saat nonce yang sudah tidak berlaku atau tidak valid digunakan atau saat beralih di antara alur kerja OAuth yang berbeda (seperti beralih dari Authorization Code Exchange awal ke permintaan Token Refresh), server Google menerapkan isolasi alur kerja. Artinya, server tanpa syarat menolak nonce dengan tantangan HTTP 400 use_dpop_nonce untuk membuat namespace nonce baru untuk alur kerja baru.

Berikut adalah contoh respons 400 yang memerlukan percobaan ulang dan bukti baru yang akan dibuat menggunakan nilai 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."
}

Jika berhasil, nonce baru dan token akses berumur pendek akan ditampilkan:

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

Simpan nilai DPoP-Nonce untuk digunakan dalam permintaan berikutnya.

Lihat Menggunakan OAuth 2.0 untuk Aplikasi Server Web dan Praktik Terbaik untuk mengetahui detail dan rekomendasi tambahan.