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.
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.
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.