En esta guía, se detalla cómo implementar DPoP (demostración de prueba de posesión) en tus integraciones de OAuth 2.0 con la plataforma de OAuth de Google. DPoP (definido en RFC 9449) protege tus aplicaciones contra el robo de tokens y los ataques de repetición vinculando criptográficamente los tokens a un par de claves asimétricas generadas por el cliente.
Cambios en el flujo de código de autorización
Agregar DPoP a un flujo de código de autorización de OAuth 2.0 existente requiere la generación y el almacenamiento de un par de claves, la construcción de un JWT de prueba de DPoP y la inclusión de la prueba como un encabezado HTTP cuando el código de autorización se intercambia por un token de actualización, como se muestra en los pasos 5 y 6 de la Figura 1.
Solicitud de código de autorización
La solicitud de autorización se crea de forma normal. Por ejemplo:
$ 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"
El código de autorización que se devuelve como un parámetro de URI de redireccionamiento se usa en la construcción de una prueba de DPoP. El token de actualización se vincula a la prueba incluida como un encabezado HTTP en todas las solicitudes adicionales al extremo del token.
Las SPA puras del cliente sin secretos no pueden usar DPoP directamente debido al
requisito de client_secret y a las limitaciones de CORS en el encabezado DPoP-Nonce. Para
proteger las SPA, enruta el tráfico a través de un backend para frontend (BFF) que actúe como un
cliente confidencial, habilite access_type=offline y use DPoP para vincular
el token de actualización del lado del servidor.
Crea la prueba de DPoP
Una prueba contiene un encabezado JOSE y una carga útil.
Para construir el encabezado, genera un par de claves EC P-256 (ES256) y, luego, incluye
las coordenadas de la clave pública (x y y) en el parámetro jwk. También se admite un par de claves RSA (RS256), aunque se recomienda ES256 debido al tamaño de firma más pequeño y al menor costo de procesamiento.
Este es un ejemplo de encabezado JOSE:
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "VC91y9ZYdfSWaDv8JaI6gx5ifOw2rn3YdqkAB51Uu6E",
"y": "ikPjOtea4k7fWPVrRYwaA4Ww6iVY3pOOICotHwwGV3o"
}
}
Para construir la carga útil de la prueba, se requieren cuatro reclamaciones (htm, htu, iat
y jti), además de una reclamación nonce opcional.
Dos reclamaciones: htm: POST y htu: https://oauth2.googleapis.com/token son valores fijos y no cambian cuando se realiza una solicitud al extremo del token de Google.
Las otras dos reclamaciones, iat y jti, se deben generar para cada solicitud. El
valor de iat es la marca de tiempo de emisión y cambia por solicitud. El valor de
la reclamación del ID de JWT (jti) depende del tipo de intercambio. Cuando un código de autorización se intercambia por tokens de acceso y tokens de actualización, el valor de jti es el hash SHA256 codificado en Base-64 y URL del código de autorización, como jti =
BASE64URL(SHA-256(authorization_code)). La reclamación nonce es opcional en el
intercambio inicial de código de autorización y solo se necesita si tu cliente ya
tiene un DPoP-Nonce válido de una solicitud anterior.
Este es un ejemplo de cuerpo de carga útil:
{
"jti": "o29CN8LIY0l_N8iy5-ilon1guad9NFQHFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"iat": 1784822025
}
El encabezado JOSE y el cuerpo de la carga útil se codifican y firman como un JWT (RFC 7519) para
su uso directo en el encabezado HTTP DPoP en la solicitud de token. Para ES256, la firma de JWS debe usar el formato IEEE P1363 sin procesar (R || S, 64 bytes para 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"
Se devuelve un token de actualización vinculado a DPoP junto con un encabezado HTTP DPoP-Nonce,
por ejemplo:
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"
}
Siempre almacena en caché y envía el DPoP-Nonce más reciente que devuelve el servidor de autorización de Google en las solicitudes de token posteriores. Los nonces vencen de forma periódica
y, si a una prueba le falta una reclamación nonce obligatoria o contiene un nonce no válido o
vencido, el servidor devuelve una respuesta HTTP 400 (use_dpop_nonce)
con un encabezado DPoP-Nonce nuevo para usar en los reintentos.
Cambios en el flujo de actualización de tokens
Para actualizar un flujo de actualización de tokens de OAuth 2.0 existente, se requiere la generación y el envío de una prueba de DPoP como encabezado HTTP cuando se intercambia un token de actualización por tokens nuevos, como se muestra en los pasos del 2 al 5 de la figura 2.
Cómo compilar la prueba de DPoP
El método para construir una prueba de actualización de token difiere de la situación del código de autorización. El encabezado JOSE se construye de la misma manera que se describió
anteriormente cuando se compila una solicitud de código de autorización. El cuerpo de la prueba se
construye de manera similar, pero incluye una reclamación nonce y jti contiene una cadena aleatoria
única.
Para construir el cuerpo de la carga útil, el valor del encabezado HTTP DPoP-Nonce que se devolvió antes
debe incluirse en la reclamación nonce y la marca de tiempo de emisión (iat)
se debe actualizar para cada solicitud. El ID de JWT (jti) es una cadena aleatoria única
que se genera por solicitud, con la API de WebCrypto integrada
crypto.getRandomValues(new Uint8Array(24)) y la codificación Base64URL de la cadena.
Este es un ejemplo de cuerpo de carga útil que contiene jti, nonce y iat:
{
"jti": "o29CN8ZIY0l_K8iy5-ilon1guad9NF6HFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"nonce": "AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI",
"iat": 1784822025
}
El encabezado JOSE y el cuerpo de la carga útil se codifican como un JWT (RFC7519) para su uso directo en el encabezado HTTP de DPoP en la solicitud de token.
La prueba se agrega como un encabezado DPoP a la solicitud de actualización del token:
$ 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"
Cuando se usa un nonce vencido o no válido, o cuando se realiza la transición entre
diferentes flujos de trabajo de OAuth (como pasar del intercambio de código de autorización
inicial a una solicitud de actualización de token), el servidor de Google aplica el aislamiento
del flujo de trabajo. Esto significa que el servidor rechaza de forma incondicional el nonce con un desafío HTTP
400 use_dpop_nonce para establecer un espacio de nombres de nonce nuevo para el
flujo de trabajo nuevo.
Este es un ejemplo de respuesta 400 que requiere un reintento y una prueba nueva que se
creará con el valor 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 se ejecuta de forma correcta, se devuelven un nonce nuevo y un token de acceso de corta duración:
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"
}
Guarda el valor de DPoP-Nonce para usarlo en la siguiente solicitud.
Consulta Usa OAuth 2.0 para aplicaciones de servidor web y Prácticas recomendadas para obtener más detalles y recomendaciones.