Guía de adopción de DPoP

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 reproducció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

Para agregar DPoP a un flujo de código de autorización de OAuth 2.0 existente, se 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 se intercambia el código de autorización por un token de actualización, como se muestra en los pasos 5 y 6 de la Figura 1.

Flujo de código de autorización con DPoP
Figura 1: Secuencia de eventos en el flujo de código de autorización con DPoP

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 muestra como un parámetro de URI de redireccionamiento se usa en la construcción de una prueba de DPoP. El token de actualización está vinculado a la prueba incluida como un encabezado HTTP en todas las solicitudes posteriores al extremo del token.

Las SPA puras y sin secretos del cliente no pueden usar DPoP directamente debido al requisito client_secret y 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 utilice DPoP para vincular el token de actualización del servidor.

Construye 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) e incluye las coordenadas de la clave pública (x y y) en el parámetro jwk. También es posible un par de claves RSA, pero no se recomienda debido a los mayores costos 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 valores.

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 de ID de JWT (jti) depende del tipo de intercambio. Cuando se intercambia un código de autorización por tokens de acceso y 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)).

Este es un ejemplo del cuerpo de la 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 como un JWT (RFC7519) para su uso directo en el encabezado HTTP DPoP en la solicitud de token:

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

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

El nonce generado por el servidor de autorización de Google se debe incluir en cada solicitud de token posterior. Ten en cuenta que un valor nonce se usa solo una vez y se rechaza un valor nonce faltante, no válido, vencido o reutilizado con una respuesta HTTP 400. En este caso, se muestra un 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 un encabezado HTTP cuando se intercambia un token de actualización por tokens nuevos, como se muestra en los pasos 2 a 5 de la Figura 2.

Flujo de actualización de tokens con DPoP
Figura 2. Secuencia de eventos en el flujo de actualización de tokens con manejo de errores y reintento

Cómo compilar la prueba de DPoP

El método para construir una prueba para la actualización de tokens difiere del caso de 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, se debe incluir el valor del encabezado HTTP DPoP-Nonce que se mostró anteriormente en la reclamación nonce y se debe actualizar la marca de tiempo de emisión (iat) 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 del cuerpo de la carga útil que contiene jti, nonce y iat:

{
  "jti": "o29CN8ZIY0l_K8iy5-ilon1gwad9NF6HFOdXTzrBNck",
  "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 DPoP en la solicitud de token.

La prueba se agrega como un encabezado DPoP a la solicitud de actualización de tokens:

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

Cuando se usa un nonce vencido, incorrecto o reutilizado, o cuando se realiza la transición entre diferentes flujos de trabajo de OAuth (como pasar del intercambio inicial de código de autorización a una solicitud de actualización de tokens), el servidor de Google aplica el aislamiento del flujo de trabajo. Esto significa que el servidor rechaza el nonce de forma incondicional con un desafío use_dpop_nonce HTTP 400 para establecer un espacio de nombres 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 construya 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 la operación se realiza correctamente, se muestran 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",
  "challenge": "AO4t07IXuovyCbtLEr6VVFZQ_Kb78MMOXTt6-CyZpsJeF62HZ3P_EW55XbWqYcU76Jg"
}

Guarda el valor DPoP-Nonce para usarlo en la próxima solicitud.

Consulta Usa OAuth 2.0 para aplicaciones de servidor web y Prácticas recomendadas para obtener más detalles y recomendaciones.