DPoP-Einführungsleitfaden

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

Änderungen am Autorisierungscode-Ablauf

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

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

Autorisierungscode-Anfrage

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 als Weiterleitungs-URI-Parameter zurückgegebene Autorisierungscode wird bei der Erstellung eines DPoP-Proofs verwendet. Das Aktualisierungstoken ist an den Proof gebunden, der in allen weiteren Anfragen an den Token-Endpunkt als HTTP-Header enthalten ist.

Reine, geheimnislose clientseitige SPAs können DPoP aufgrund der Anforderung client_secret und der CORS-Beschränkungen für den DPoP-Nonce-Header nicht direkt verwenden. Um SPAs zu schützen, 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 ist ebenfalls möglich, wird aber aufgrund der höheren Rechenkosten nicht empfohlen.

Beispiel für einen JOSE-Header:

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

Zum Erstellen der Proof-Nutzlast sind vier Werte erforderlich.

Zwei Ansprüche: 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 anderen beiden Ansprüche: iat und jti müssen für jede Anfrage generiert werden. Der Wert von iat ist der Zeitstempel für die Ausstellung und ändert sich pro Anfrage. Der Wert des JWT-ID-Anspruchs (jti) hängt vom Typ 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)).

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 (RFC7519) codiert, um sie direkt im DPoP-HTTP-Header in der Token-Anfrage zu verwenden:

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

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

Die vom Autorisierungsserver von Google generierte Nonce muss in jeder nachfolgenden Token-Anfrage enthalten sein. Ein Nonce-Wert wird nur einmal verwendet. Ein fehlender, ungültiger, abgelaufener oder wiederverwendeter Nonce-Wert wird mit einer HTTP-Antwort 400 abgelehnt. In diesem Fall wird eine neue Nonce für Wiederholungsversuche zurückgegeben.

Änderungen am Token-Aktualisierungsablauf

Wenn Sie einen vorhandenen OAuth 2.0-Token-Aktualisierungsablauf aktualisieren möchten, 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 bis 5 von Abbildung 2 dargestellt.

Ablauf der Tokenaktualisierung mit DPoP
Abbildung 2. Die Abfolge der Ereignisse im Token-Aktualisierungsablauf mit Fehlerbehandlung und Wiederholung.

DPoP-Proof erstellen

Die Methode zum Erstellen eines Proofs für die Token-Aktualisierung unterscheidet sich vom Szenario mit dem Autorisierungscode. Der JOSE-Header wird auf dieselbe Weise erstellt wie zuvor beim Erstellen einer Autorisierungscode-Anfrage. Der Proof-Text wird ähnlich erstellt, enthält aber einen nonce-Anspruch und jti enthält einen eindeutigen zufälligen String.

Um den Nutzlasttext zu erstellen, muss der zuvor zurückgegebene DPoP-Nonce-HTTP-Headerwert in den nonce-Anspruch 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.

Beispiel für einen Nutzlasttext mit jti, nonce und iat:

{
  "jti": "o29CN8ZIY0l_K8iy5-ilon1gwad9NF6HFOdXTzrBNck",
  "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 sie direkt im DPoP-HTTP-Header in der Token-Anfrage zu verwenden.

Der Proof wird als DPoP-Header zur Token-Aktualisierungsanfrage hinzugefügt:

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

Wenn eine abgelaufene, falsche oder wiederverwendete Nonce verwendet wird oder wenn Sie zwischen verschiedenen OAuth-Workflows wechseln (z. B. vom anfänglichen Autorisierungscode-Austausch zu einer Token-Aktualisierungsanfrage), erzwingt der Server von Google 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 Ablauf einzurichten.

Beispiel für eine 400-Antwort, die einen Wiederholungsversuch und einen neuen Proof erfordert, der mit dem Wert DPoP-Nonce erstellt wird:

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",
  "challenge": "AO4t07IXuovyCbtLEr6VVFZQ_Kb78MMOXTt6-CyZpsJeF62HZ3P_EW55XbWqYcU76Jg"
}

Speichern Sie den Wert DPoP-Nonce für die Verwendung in der nächsten Anfrage.

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