Ce guide explique comment implémenter DPoP (Demonstrating Proof-of-Possession) dans vos intégrations OAuth 2.0 avec la plate-forme OAuth de Google. DPoP (défini dans la RFC 9449) protège vos applications contre le vol de jetons et les attaques par rejeu 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 jeton 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 indiqué aux étapes 5 et 6 de la figure 1.
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 construction 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 du jeton.
Les SPA côté client pures et sans secret ne peuvent pas utiliser directement DPoP 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 frontend (BFF) qui agit en tant que 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 construire 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 (RS256) est également acceptée, bien que ES256 soit recommandé en raison de la taille de signature plus petite et du coût de calcul inférieur.
Voici un exemple d'en-tête JOSE :
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "VC91y9ZYdfSWaDv8JaI6gx5ifOw2rn3YdqkAB51Uu6E",
"y": "ikPjOtea4k7fWPVrRYwaA4Ww6iVY3pOOICotHwwGV3o"
}
}
Pour construire la charge utile de preuve, quatre revendications sont requises (htm, htu, iat et jti), ainsi qu'une revendication nonce facultative.
Deux revendications : htm: POST et htu: https://oauth2.googleapis.com/token sont des valeurs fixes et ne changent pas lors d'une requête adressée au 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 au code temporel d'émission et change à chaque requête. La valeur de la revendication d'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 est le hachage SHA256 encodé en Base64 et en URL du code d'autorisation, par exemple jti =
BASE64URL(SHA-256(authorization_code)). Une revendication nonce est facultative lors de l'échange initial du code d'autorisation. Elle n'est nécessaire que si votre client détient déjà un DPoP-Nonce valide provenant d'une requête précédente.
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 et signés en tant que jeton JWT (RFC 7519) pour une utilisation directe dans l'en-tête HTTP DPoP de la requête de jeton. Pour ES256, la signature JWS doit utiliser le format brut IEEE P1363 (R || S, 64 octets pour 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"
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"
}
Mettez toujours en cache et envoyez le DPoP-Nonce le plus récent renvoyé par le serveur d'autorisation de Google dans les requêtes de jeton ultérieures. Les nonces expirent régulièrement. Si une preuve ne contient pas de revendication nonce requise, ou si elle contient un nonce non valide ou expiré, le serveur renvoie une réponse HTTP 400 (use_dpop_nonce) avec un nouvel en-tête DPoP-Nonce à utiliser pour les nouvelles tentatives.
Modifications du flux d'actualisation des jetons
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 indiqué aux étapes 2 à 5 de la figure 2.
Créer la preuve DPoP
La méthode de construction d'une preuve pour l'actualisation du jeton diffère de celle du scénario de code d'autorisation. L'en-tête JOSE est construit de la même manière que celle décrite précédemment lors de la création d'une requête de code d'autorisation. Le corps de la preuve est construit de la même manière, mais inclut une revendication nonce, et jti contient une chaîne aléatoire unique.
Pour construire le corps de la charge utile, la valeur d'en-tête HTTP DPoP-Nonce précédemment renvoyée doit être incluse dans la revendication nonce et le code temporel 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-ilon1guad9NF6HFOdXTzrBNck",
"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 jeton 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 du jeton :
$ 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"
Lorsqu'un nonce expiré ou non valide est utilisé, ou lors de la transition entre différents workflows OAuth (par exemple, lors du passage de l'échange de code d'autorisation initial à une requête d'actualisation de jeton), le serveur de Google applique l'isolation des workflows. Cela signifie que le serveur rejette sans condition le nonce avec un test 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 une nouvelle preuve à construire à 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."
}
Si l'opération réussit, un nouveau nonce et un jeton d'accès éphémère 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"
}
Enregistrez la valeur DPoP-Nonce pour l'utiliser dans la requête suivante.
Pour en savoir plus et obtenir des recommandations, consultez Utiliser OAuth 2.0 pour les applications de serveur Web et Bonnes pratiques.