DPoP-Einführungsleitfaden

In diesem Leitfaden wird beschrieben, wie Sie DPoP (Demonstrating Proof-of-Possession) in Ihren OAuth 2.0-Integrationen mit der OAuth-Plattform von Google implementieren. DPoP (definiert in RFC 9449) schützt Ihre Anwendungen vor Tokendiebstahl und Replay-Angriffen, indem Tokens kryptografisch an ein vom Client generiertes asymmetrisches Schlüsselpaar gebunden werden.

Änderungen am Autorisierungscode-Vorgang

Wenn Sie DPoP zu einem vorhandenen OAuth 2.0-Autorisierungscode-Vorgang hinzufügen möchten, müssen Sie ein Schlüsselpaar generieren und speichern, ein DPoP-Proof-JWT erstellen und den Proof als HTTP-Header einfügen, wenn der Autorisierungscode gegen ein Aktualisierungstoken ausgetauscht wird, wie in den Schritten 5 und 6 von Abbildung 1 dargestellt.

Autorisierungscode-Vorgang mit DPoP
Abbildung 1. Die Abfolge der Ereignisse im Autorisierungscode-Vorgang mit DPoP.

Anfrage für Autorisierungscode

Die Autorisierungsanfrage wird normal erstellt. Beispiel:

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

Der Autorisierungscode, der als Weiterleitungs-URI-Parameter zurückgegeben wird, wird beim Erstellen eines DPoP-Proofs verwendet. Das Aktualisierungstoken ist an den Nachweis gebunden, der als HTTP-Header in allen weiteren Anfragen an den Token-Endpunkt enthalten ist.

Reine, geheimnislose clientseitige SPAs können DPoP aufgrund der client_secret-Anforderung und der CORS-Einschränkungen für den DPoP-Nonce-Header nicht direkt verwenden. Um SPAs zu sichern, leiten Sie den Traffic über ein Backend-for-Frontend (BFF) weiter, das als vertraulicher Client fungiert, access_type=offline aktiviert und DPoP verwendet, um das Aktualisierungstoken serverseitig zu binden.

DPoP-Proof erstellen

Ein Proof enthält einen JOSE-Header und eine Nutzlast.

Generieren Sie zum Erstellen des Headers ein EC-P-256-Schlüsselpaar (ES256) und fügen Sie die Koordinaten des öffentlichen Schlüssels (x und y) in den Parameter jwk ein. Ein RSA-Schlüsselpaar (RS256) wird ebenfalls unterstützt, aber ES256 wird aufgrund der kleineren Signaturgröße und der geringeren Rechenkosten empfohlen.

Hier ein Beispiel für einen JOSE-Header:

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

Zum Erstellen der Nutzlast für den Nachweis sind vier Anforderungen erforderlich (htm, htu, iat und jti) sowie eine optionale nonce-Anforderung.

Zwei Anforderungen: htm: POST und htu: https://oauth2.googleapis.com/token sind feste Werte und ändern sich nicht, wenn eine Anfrage an den Token-Endpunkt von Google gesendet wird.

Die beiden anderen Claims, iat und jti, müssen für jede Anfrage generiert werden. Der Wert von iat ist der Zeitstempel der Ausstellung und ändert sich pro Anfrage. Der Wert des JWT-ID-Anspruchs (jti) hängt von der Art des Austauschs ab. Wenn ein Autorisierungscode gegen Zugriffs- und Aktualisierungstokens ausgetauscht wird, ist der Wert von jti der Base64- und URL-codierte SHA256-Hash des Autorisierungscodes, z. B. jti = BASE64URL(SHA-256(authorization_code)). Ein nonce-Anspruch ist beim ersten Autorisierungscodeaustausch optional und nur erforderlich, wenn Ihr Client bereits eine gültige DPoP-Nonce aus einer vorherigen Anfrage hat.

Dies ist ein Beispiel für einen Nutzlasttext:

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

Der JOSE-Header und der Nutzlasttext werden als JWT (RFC 7519) codiert und signiert, um direkt im HTTP-Header DPoP in der Tokenanfrage verwendet zu werden. Für ES256 muss die JWS-Signatur das Rohformat IEEE P1363 (R || S, 64 Byte für P-256) verwenden:

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

Ein DPoP-gebundenes Aktualisierungstoken wird zusammen mit einem DPoP-Nonce-HTTP-Header zurückgegeben, z. B.:

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

Speichere immer das neueste DPoP-Nonce, das vom Autorisierungsserver von Google zurückgegeben wurde, im Cache und sende es in nachfolgenden Tokenanfragen. Nonces laufen regelmäßig ab. Wenn in einem Nachweis ein erforderlicher nonce-Anspruch fehlt oder eine ungültige oder abgelaufene Nonce enthalten ist, gibt der Server eine HTTP-400-Antwort (use_dpop_nonce) mit einem neuen DPoP-Nonce-Header zurück, der für Wiederholungsversuche verwendet werden kann.

Änderungen am Ablauf der Tokenaktualisierung

Wenn Sie einen vorhandenen OAuth 2.0-Token-Aktualisierungsablauf aktualisieren, müssen Sie einen DPoP-Proof als HTTP-Header generieren und senden, wenn ein Aktualisierungstoken gegen neue Tokens ausgetauscht wird. Dies ist in den Schritten 2–5 von Abbildung 2 dargestellt.

Tokenaktualisierungsablauf mit DPoP
Abbildung 2. Die Abfolge der Ereignisse im Tokenaktualisierungsablauf mit Fehlerbehandlung und Wiederholung.

DPoP-Proof erstellen

Die Methode zum Erstellen eines Nachweises für die Tokenaktualisierung unterscheidet sich vom Autorisierungscode-Szenario. Der JOSE-Header wird auf dieselbe Weise erstellt, wie zuvor beim Erstellen einer Autorisierungscodeanfrage beschrieben. Der Proof-Text ist ähnlich aufgebaut, enthält aber eine nonce-Anforderung und jti enthält einen eindeutigen zufälligen String.

Zum Erstellen des Nutzlasttexts muss der zuvor zurückgegebene HTTP-Headerwert DPoP-Nonce in den Anspruch nonce aufgenommen und der Zeitstempel für die Ausstellung (iat) für jede Anfrage aktualisiert werden. Die JWT-ID (jti) ist ein eindeutiger zufälliger String, der pro Anfrage mit der integrierten WebCrypto API crypto.getRandomValues(new Uint8Array(24)) generiert und mit Base64URL codiert wird.

Dies ist ein Beispiel für einen Nutzlasttext, der jti, nonce und iat enthält:

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

Der JOSE-Header und der Nutzlasttext werden als JWT (RFC7519) codiert, um direkt im DPoP-HTTP-Header in der Tokenanfrage verwendet zu werden.

Der Nachweis wird der Anfrage zur Tokenaktualisierung als DPoP-Header hinzugefügt:

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

Wenn eine abgelaufene oder ungültige Nonce verwendet wird oder wenn zwischen verschiedenen OAuth-Workflows gewechselt wird (z. B. vom anfänglichen Austausch des Autorisierungscodes zu einer Anfrage zur Tokenaktualisierung), erzwingt der Google-Server die Workflow-Isolierung. Das bedeutet, dass der Server die Nonce bedingungslos mit einer HTTP-400-use_dpop_nonce-Challenge ablehnt, um einen neuen Nonce-Namespace für den neuen Workflow einzurichten.

Dies ist ein Beispiel für eine 400-Antwort, die einen Wiederholungsversuch und einen neuen Nachweis erfordert, der mit dem Wert DPoP-Nonce erstellt werden muss:

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

Bei Erfolg werden eine neue Nonce und ein kurzlebiges Zugriffstoken zurückgegeben:

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

Speichern Sie den Wert DPoP-Nonce für die nächste Anfrage.

Weitere Informationen und Empfehlungen finden Sie unter OAuth 2.0 für Webserveranwendungen verwenden und Best Practices.