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 na RFC 9449) protege seus aplicativos contra roubo de tokens e ataques de repetição, vinculando criptograficamente os 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 de 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.
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 construção de uma prova de 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 do lado do cliente sem secrets não podem usar o DPoP diretamente devido ao
requisito client_secret e às limitações de CORS no cabeçalho DPoP-Nonce. Para
proteger SPAs, encaminhe o tráfego por um back-end para front-end (BFF) que atua como um
cliente confidencial, ativa access_type=offline e usa DPoP para vincular
o token de atualização no lado do servidor.
Criar a prova de 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 (RS256) também é aceito, mas ES256 é recomendado devido ao tamanho menor da assinatura e ao menor custo computacional.
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, são necessárias quatro declarações (htm, htu, iat
e jti), além de uma declaração nonce opcional.
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 ao 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 de emissão e muda de acordo com a 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 token de atualização, o valor de jti é o
hash SHA256 codificado em Base64 e URL do código de autorização, como jti =
BASE64URL(SHA-256(authorization_code)). Uma declaração nonce é opcional na
troca de código de autorização inicial e só é necessária se o cliente já
tiver um DPoP-Nonce válido de uma solicitação anterior.
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 e assinados como um JWT (RFC 7519) para
uso direto no cabeçalho HTTP DPoP na solicitação de token. Para ES256, a assinatura JWS
precisa usar o formato IEEE P1363 bruto (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"
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"
}
Sempre armazene em cache e envie o DPoP-Nonce mais recente retornado pelo servidor de autorização do Google nas solicitações de token subsequentes. Os nonces expiram periodicamente.
Se uma prova não tiver uma declaração nonce obrigatória ou contiver um nonce inválido ou
expirado, o servidor vai retornar uma resposta HTTP 400 (use_dpop_nonce)
com um novo cabeçalho DPoP-Nonce para uso em novas tentativas.
Mudanças no fluxo de atualização de token
Para atualizar um fluxo de atualização de token OAuth 2.0, é necessário gerar e enviar uma prova de 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.
Como criar a prova de DPoP
O método para criar uma prova de atualização de token é diferente do cenário do código de autorização. O cabeçalho JOSE é construído da mesma maneira descrita
anteriormente ao criar uma solicitação de código de autorização. O corpo da prova é
construído 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 de emissão (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 codificando a string em Base64URL.
Este é um exemplo de corpo de payload que contém jti, nonce e iat:
{
"jti": "o29CN8ZIY0l_K8iy5-ilon1guad9NF6HFOdXTzrBNck",
"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 do DPoP na solicitação de token.
A prova é adicionada como um cabeçalho DPoP à solicitação de atualização do 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"
Quando um valor de uso único expirado ou inválido é usado ou ao fazer a transição entre
diferentes fluxos de trabalho do OAuth (como passar da troca de código de autorização
inicial 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 HTTP
400 use_dpop_nonce 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
construída 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."
}
Se a operação for bem-sucedida, um novo valor de uso único e um token de acesso de curta duração serã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"
}
Salve o valor DPoP-Nonce para usar 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.