Guia de adoção do DPoP

Este guia detalha como implementar o DPoP (Demonstrating Proof-of-Possession) nas suas integrações do OAuth 2.0 com a plataforma OAuth do Google. O DPoP (definido em RFC 9449) protege seus aplicativos contra roubo de tokens e ataques de repetição, vinculando criptograficamente tokens a um par de chaves assimétricas gerado pelo cliente.

Mudanças no fluxo do código de autorização

Para adicionar o DPoP a um fluxo de código de autorização do OAuth 2.0, é necessário gerar e armazenar um par de chaves, criar um JWT de prova do DPoP e incluir a prova como um cabeçalho HTTP quando o código de autorização for trocado por um token de atualização, conforme mostrado nas etapas 5 e 6 da Figura 1.

Fluxo do código de autorização com DPoP
Figura 1. A sequência de eventos no fluxo do código de autorização usando o DPoP.

Solicitação de código de autorização

A solicitação de autorização é criada normalmente. Exemplo:

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

O código de autorização retornado como um parâmetro de URI de redirecionamento é usado na criação de uma prova do DPoP. O token de atualização é vinculado à prova incluída como um cabeçalho HTTP em todas as solicitações futuras para o endpoint do token.

Os SPAs puros e sem segredo do lado do cliente não podem usar o DPoP diretamente devido ao requisito client_secret e às limitações do CORS no cabeçalho DPoP-Nonce. Para proteger os SPAs, encaminhe o tráfego por um back-end para front-end (BFF) que atue como um cliente confidencial, ative access_type=offline e use o DPoP para vincular o token de atualização no lado do servidor.

Criar a prova do DPoP

Uma prova contém um cabeçalho JOSE e um payload.

Para criar o cabeçalho, gere um par de chaves EC P-256 (ES256) e inclua as coordenadas da chave pública (x e y) no parâmetro jwk. Um par de chaves RSA também é possível, mas não recomendado devido aos custos computacionais mais altos.

Este é um exemplo de cabeçalho JOSE:

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

Para criar o payload de prova, quatro valores são necessários.

Duas declarações: htm: POST e htu: https://oauth2.googleapis.com/token são valores fixos e não mudam ao fazer uma solicitação para o endpoint de token do Google.

As outras duas declarações: iat e jti precisam ser geradas para cada solicitação. O valor de iat é o carimbo de data/hora emitido e muda por solicitação. O valor da declaração do ID do JWT (jti) depende do tipo de troca. Quando um código de autorização é trocado por tokens de acesso e atualização, o valor de jti é o hash SHA256 codificado em Base-64 e URL do código de autorização, como jti = BASE64URL(SHA-256(authorization_code)).

Este é um exemplo de corpo de payload:

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

O cabeçalho JOSE e o corpo do payload são codificados como um JWT (RFC7519) para uso direto no cabeçalho HTTP DPoP na solicitação 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"

Um token de atualização vinculado ao DPoP é retornado com um cabeçalho HTTP DPoP-Nonce, por exemplo:

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

O valor de uso único gerado pelo servidor de autorização do Google precisa ser incluído em todas as solicitações de token subsequentes. Um valor de uso único é usado apenas uma vez, e um valor ausente, inválido, expirado ou reutilizado é rejeitado com uma resposta HTTP 400. Nesse caso, um novo valor de uso único é retornado para uso em novas tentativas.

Mudanças no fluxo de atualização de token

Para atualizar um fluxo de atualização de token do OAuth 2.0, é necessário gerar e enviar uma prova do DPoP como um cabeçalho HTTP quando um token de atualização é trocado por novos tokens, conforme mostrado nas etapas 2 a 5 da Figura 2.

Fluxo de atualização de token com DPoP
Figura 2. A sequência de eventos no fluxo de atualização de token com tratamento de erros e nova tentativa.

Criar a prova do DPoP

O método para criar uma prova para atualização de token é diferente do cenário de código de autorização. O cabeçalho JOSE é criado da mesma maneira descrita anteriormente ao criar uma solicitação de código de autorização. O corpo da prova é criado de maneira semelhante, mas inclui uma declaração nonce e jti contém uma string aleatória exclusiva.

Para criar o corpo do payload, o valor do cabeçalho HTTP DPoP-Nonce retornado anteriormente precisa ser incluído na declaração nonce, e o carimbo de data/hora emitido (iat) precisa ser atualizado para cada solicitação. O ID do JWT (jti) é uma string aleatória exclusiva gerada por solicitação, usando a API WebCrypto integrada crypto.getRandomValues(new Uint8Array(24)) e a codificação Base64URL da string.

Este é um exemplo de corpo de payload que contém jti, nonce e iat:

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

O cabeçalho JOSE e o corpo do payload são codificados como um JWT (RFC7519) para uso direto no cabeçalho HTTP DPoP na solicitação de token.

A prova é adicionada como um cabeçalho DPoP à solicitação de atualização de token:

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

Quando um valor de uso único expirado, incorreto ou reutilizado é usado ou quando há uma transição entre diferentes fluxos de trabalho do OAuth (como passar da troca inicial de código de autorização para uma solicitação de atualização de token), o servidor do Google aplica o isolamento do fluxo de trabalho. Isso significa que o servidor rejeita incondicionalmente o valor de uso único com um desafio use_dpop_nonce HTTP 400 para estabelecer um novo namespace de valor de uso único para o novo fluxo de trabalho.

Este é um exemplo de resposta 400 que exige uma nova tentativa e uma nova prova a ser criada usando o 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."
}

Em caso de sucesso, um novo valor de uso único e um token de acesso de curta duração são retornados:

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

Salve o valor DPoP-Nonce para uso na próxima solicitação.

Consulte Como usar o OAuth 2.0 para aplicativos de servidor da Web e Práticas recomendadas para mais detalhes e recomendações.