Guide d'adoption de DPoP

Ce guide explique comment mettre en œuvre DPoP (Demonstrating Proof-of-Possession) dans vos intégrations OAuth 2.0 avec la plate-forme OAuth de Google. DPoP (défini dans la norme RFC 9449) protège vos applications contre le vol de jetons et les attaques par relecture en liant de manière cryptographique les jetons à une paire de clés asymétriques générée par le client.

Modifications du flux avec code d'autorisation

Pour ajouter DPoP à un flux avec code d'autorisation OAuth 2.0 existant, vous devez générer et stocker une paire de clés, créer un JWT de preuve DPoP et inclure la preuve en tant qu'en-tête HTTP lorsque le code d'autorisation est échangé contre un jeton d'actualisation, comme illustré aux étapes 5 et 6 de la figure 1.

Flux avec code d'autorisation avec DPoP
Figure 1. Séquence d'événements dans le flux avec code d'autorisation à l'aide de DPoP.

Demande de code d'autorisation

La demande d'autorisation est créée normalement. Exemple :

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

Le code d'autorisation renvoyé en tant que paramètre d'URI de redirection est utilisé dans la création d'une preuve DPoP. Le jeton d'actualisation est lié à la preuve incluse en tant qu'en-tête HTTP dans toutes les requêtes ultérieures adressées au point de terminaison de jeton.

Les SPA côté client pures et sans secret ne peuvent pas utiliser DPoP directement en raison de l'exigence client_secret et des limites CORS sur l'en-tête DPoP-Nonce. Pour sécuriser les SPA, acheminez le trafic via un backend pour le frontend (BFF) qui fait office de client confidentiel, active access_type=offline et utilise DPoP pour lier le jeton d'actualisation côté serveur.

Créer la preuve DPoP

Une preuve contient un en-tête JOSE et une charge utile.

Pour créer l'en-tête, générez une paire de clés EC P-256 (ES256) et incluez les coordonnées de la clé publique (x et y) dans le paramètre jwk. Une paire de clés RSA est également possible, mais n'est pas recommandée en raison de coûts de calcul plus élevés.

Voici un exemple d'en-tête JOSE :

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

Pour créer la charge utile de la preuve, quatre valeurs sont requises.

Deux revendications : htm: POST et htu: https://oauth2.googleapis.com/token sont des valeurs fixes et ne changent pas lorsque vous effectuez une requête auprès du point de terminaison de jeton de Google.

Les deux autres revendications, iat et jti, doivent être générées pour chaque requête. La valeur de iat correspond à l'horodatage d'émission et change pour chaque requête. La valeur de la revendication de l'ID JWT (jti) dépend du type d'échange. Lorsqu'un code d'autorisation est échangé contre des jetons d'accès et d'actualisation, la valeur de jti correspond au hachage SHA256 encodé en base64 et en URL du code d'autorisation, tel que jti = BASE64URL(SHA-256(authorization_code)).

Voici un exemple de corps de charge utile :

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

L'en-tête JOSE et le corps de la charge utile sont encodés en tant que JWT (RFC7519) pour une utilisation directe dans l'en-tête HTTP DPoP de la requête de jeton :

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

Un jeton d'actualisation lié à DPoP est renvoyé avec un en-tête HTTP DPoP-Nonce, par exemple :

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

Le nonce généré par le serveur d'autorisation de Google doit être inclus dans chaque requête de jeton ultérieure. Notez qu'une valeur de nonce n'est utilisée qu'une seule fois et qu'une valeur de nonce manquante, non valide, expirée ou réutilisée est rejetée avec une réponse HTTP 400. Dans ce cas, un nouveau nonce est renvoyé pour être utilisé lors des nouvelles tentatives.

Modifications du flux d'actualisation de jeton

Pour mettre à jour un flux d'actualisation de jeton OAuth 2.0 existant, vous devez générer et envoyer une preuve DPoP en tant qu'en-tête HTTP lorsqu'un jeton d'actualisation est échangé contre de nouveaux jetons, comme illustré aux étapes 2 à 5 de la figure 2.

Flux d'actualisation de jeton avec DPoP
Figure 2. Séquence d'événements dans le flux d'actualisation de jeton avec gestion des erreurs et nouvelle tentative.

Créer la preuve DPoP

La méthode de création d'une preuve pour l'actualisation de jeton diffère du scénario de code d'autorisation. L'en-tête JOSE est créé de la même manière que celle décrite précédemment lors de la création d'une demande de code d'autorisation. Le corps de la preuve est créé de manière similaire, mais inclut une revendication nonce et jti contient une chaîne aléatoire unique.

Pour créer le corps de la charge utile, la valeur d'en-tête HTTP DPoP-Nonce renvoyée précédemment doit être incluse dans la revendication nonce, et l'horodatage d'émission (iat) doit être mis à jour pour chaque requête. L'ID JWT (jti) est une chaîne aléatoire unique générée par requête à l'aide de l'API WebCrypto intégrée crypto.getRandomValues(new Uint8Array(24)) et de l'encodage Base64URL de la chaîne.

Voici un exemple de corps de charge utile contenant jti, nonce et iat :

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

L'en-tête JOSE et le corps de la charge utile sont encodés en tant que JWT (RFC7519) pour une utilisation directe dans l'en-tête HTTP DPoP de la requête de jeton.

La preuve est ajoutée en tant qu'en-tête DPoP à la requête d'actualisation de jeton :

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

Lorsqu'un nonce expiré, incorrect ou réutilisé est utilisé, ou lors du passage d'un workflow OAuth à un autre (par exemple, lors du passage de l'échange initial de code d'autorisation à une requête d'actualisation de jeton), le serveur de Google applique l'isolation du workflow. Cela signifie que le serveur rejette inconditionnellement le nonce avec un défi HTTP 400 use_dpop_nonce pour établir un nouvel espace de noms de nonce pour le nouveau workflow.

Voici un exemple de réponse 400 qui nécessite une nouvelle tentative et la création d'une nouvelle preuve à l'aide de la valeur 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."
}

En cas de réussite, un nouveau nonce et un jeton d'accès à courte durée de vie sont renvoyés :

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

Enregistrez la valeur DPoP-Nonce pour l'utiliser dans la requête suivante.

Pour plus d'informations et de recommandations, consultez Utiliser OAuth 2.0 pour les applications de serveur Web et Bonnes pratiques.