DPoP Kullanım Kılavuzu

Bu kılavuzda, Google'ın OAuth platformuyla OAuth 2.0 entegrasyonlarınızda DPoP'yi (Sahiplik Kanıtı Gösterme) nasıl uygulayacağınız ayrıntılı olarak açıklanmaktadır. DPoP (RFC 9449'da tanımlanmıştır), jetonları istemci tarafından oluşturulan asimetrik bir anahtar çiftine kriptografik olarak bağlayarak uygulamalarınızı jeton hırsızlığına ve tekrar saldırılarına karşı korur.

Yetkilendirme kodu akışı değişiklikleri

Mevcut bir OAuth 2.0 yetkilendirme kodu akışına DPoP eklemek için anahtar çifti oluşturulması ve depolanması, DPoP kanıtı JWT'sinin oluşturulması ve yetkilendirme kodu bir yenileme jetonuyla değiştirilirken kanıtın HTTP üstbilgisi olarak eklenmesi gerekir. Bu adımlar, Şekil 1'deki 5. ve 6. adımlarda gösterilmiştir.

DPoP ile Yetkilendirme Kodu Akışı
Şekil 1. DPoP kullanan yetkilendirme kodu akışındaki etkinliklerin sırası.

Yetkilendirme kodu isteği

Yetkilendirme isteği normal şekilde oluşturulur. Örneğin:

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

Yönlendirme URI'si parametresi olarak döndürülen yetkilendirme kodu, DPoP kanıtı oluşturmak için kullanılır. Yenileme jetonu, jeton uç noktasına gönderilen diğer tüm isteklerde HTTP üstbilgisi olarak eklenen kanıta bağlıdır.

Saf, gizli anahtarsız istemci tarafı SPA'lar, client_secret şartı ve DPoP-Nonce üstbilgisindeki CORS sınırlamaları nedeniyle DPoP'yi doğrudan kullanamaz. Tek sayfalık uygulamaların güvenliğini sağlamak için trafiği, gizli istemci görevi gören, access_type=offline özelliğini etkinleştiren ve yenileme jetonunu sunucu tarafında bağlamak için DPoP'yi kullanan bir Backend-for-Frontend (BFF) üzerinden yönlendirin.

DPoP kanıtını oluşturma

Kanıt, bir JOSE başlığı ve bir yük içerir.

Üstbilgiyi oluşturmak için bir EC P-256 (ES256) anahtar çifti oluşturun ve jwk parametresine ortak anahtar koordinatlarını (x ve y) ekleyin. RSA anahtar çifti (RS256) de desteklenir ancak daha küçük imza boyutu ve daha düşük hesaplama maliyeti nedeniyle ES256 önerilir.

Aşağıda örnek bir JOSE üstbilgisi verilmiştir:

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

Kanıt yükünü oluşturmak için dört hak talebi (htm, htu, iat ve jti) ve isteğe bağlı bir nonce hak talebi gerekir.

İki hak talebi: htm: POST ve htu: https://oauth2.googleapis.com/token sabit değerlerdir ve Google'ın jeton uç noktasına istek gönderilirken değişmez.

Diğer iki hak talebi (iat ve jti) her istek için oluşturulmalıdır. iat değeri, yayınlanma zaman damgasıdır ve isteğe göre değişir. JWT kimliği (jti) talebinin değeri, exchange türüne bağlıdır. Bir yetkilendirme kodu, erişim ve yenileme jetonlarıyla değiştirildiğinde jti değeri, yetkilendirme kodunun Base-64 ve URL olarak kodlanmış SHA256 karmasıdır (ör. jti = BASE64URL(SHA-256(authorization_code))). İlk yetkilendirme kodu değişiminde nonce talebi isteğe bağlıdır ve yalnızca istemcinizin önceki bir istekten geçerli bir DPoP-Nonce'si varsa gereklidir.

Örnek yük gövdesi:

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

JOSE üstbilgisi ve yük gövdesi, jeton isteğindeki DPoP HTTP üstbilgisinde doğrudan kullanılmak üzere JWT (RFC 7519) olarak kodlanır ve imzalanır. ES256 için JWS imzasında ham IEEE P1363 biçimi (R || S, P-256 için 64 bayt) kullanılmalıdır:

$ 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'ye bağlı yenileme jetonu, DPoP-Nonce HTTP üstbilgisiyle birlikte döndürülür. Örneğin:

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

Google'ın yetkilendirme sunucusu tarafından döndürülen en son DPoP-Nonce değerini her zaman önbelleğe alın ve sonraki jeton isteklerinde gönderin. Tek seferlik rastgele sayıların süresi düzenli olarak dolar. Bir kanıtta gerekli nonce talebi eksikse veya geçersiz ya da süresi dolmuş bir tek seferlik rastgele sayı varsa sunucu, yeniden denemelerde kullanılmak üzere yeni bir DPoP-Nonce üstbilgisi içeren bir HTTP 400 (use_dpop_nonce) yanıtı döndürür.

Jeton yenileme akışındaki değişiklikler

Mevcut bir OAuth 2.0 jeton yenileme akışını güncellemek için, Şekil 2'deki 2-5. adımlarda gösterildiği gibi, yenileme jetonu yeni jetonlarla değiştirilirken bir DPoP kanıtının oluşturulması ve HTTP üstbilgisi olarak gönderilmesi gerekir.

DPoP ile Jeton Yenileme Akışı
Şekil 2. Hata işleme ve yeniden deneme ile Jeton Yenileme akışındaki etkinliklerin sırası.

DPoP kanıtını oluşturma

Jeton yenileme için kanıt oluşturma yöntemi, yetkilendirme kodu senaryosundan farklıdır. JOSE başlığı, daha önce yetkilendirme kodu isteği oluşturma bölümünde açıklandığı şekilde oluşturulur. Kanıt gövdesi benzer şekilde oluşturulur ancak bir nonce talebi içerir ve jti benzersiz bir rastgele dize içerir.

Yük gövdesini oluşturmak için daha önce döndürülen DPoP-Nonce HTTP üstbilgisi değeri nonce talebine dahil edilmeli ve yayınlanma zaman damgası (iat) her istek için güncellenmelidir. JWT kimliği (jti), yerleşik WebCrypto API'si crypto.getRandomValues(new Uint8Array(24)) kullanılarak ve dize Base64URL ile kodlanarak istek başına oluşturulan benzersiz bir rastgele dizedir.

jti, nonce ve iat içeren örnek bir yük gövdesi:

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

JOSE üstbilgisi ve yük gövdesi, jeton isteğindeki DPoP HTTP üstbilgisinde doğrudan kullanılmak üzere JWT (RFC7519) olarak kodlanır.

Kanıt, jeton yenileme isteğine DPoP başlığı olarak eklenir:

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

Süresi dolmuş veya geçersiz bir tek seferlik rastgele sayı kullanıldığında ya da farklı OAuth iş akışları arasında geçiş yapıldığında (ör. ilk yetkilendirme kodu değişiminden jeton yenileme isteğine geçiş) Google'ın sunucusu iş akışı izolasyonunu zorunlu kılar. Bu, sunucunun yeni iş akışı için yeni bir tek seferlik rastgele sayı ad alanı oluşturmak üzere HTTP 400 use_dpop_nonce sorgulamasıyla tek seferlik rastgele sayıyı koşulsuz olarak reddettiği anlamına gelir.

Bu, yeniden deneme ve DPoP-Nonce değeri kullanılarak yeni bir kanıt oluşturulmasını gerektiren örnek bir 400 yanıtıdır:

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

İşlem başarılı olursa yeni bir tek seferlik rastgele sayı ve kısa süreli erişim jetonu döndürülür:

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

Bir sonraki istekte kullanmak için DPoP-Nonce değerini kaydedin.

Ek ayrıntılar ve öneriler için Web Sunucusu Uygulamaları için OAuth 2.0'ı Kullanma ve En İyi Uygulamalar başlıklı makalelere bakın.