В этом руководстве рассказывается, как реализовать DPoP (Demonstrating Proof-of-Possession) в интеграциях OAuth 2.0 с платформой OAuth от Google. DPoP (определено в RFC 9449) защищает ваши приложения от кражи токенов и атак с повторной отправкой, криптографически связывая токены с асимметричной парой ключей, созданной клиентом.
Изменения в обработке кода авторизации
Чтобы добавить DPoP в существующий процесс обработки кода авторизации OAuth 2.0, необходимо сгенерировать и сохранить пару ключей, создать JWT-токен подтверждения DPoP и включить его в HTTP-заголовок при обмене кода авторизации на токен обновления, как показано на шагах 5 и 6 на рисунке 1.
Запрос кода авторизации
Запрос на авторизацию создается обычным образом. Пример:
$ 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"
Код авторизации, возвращенный в качестве параметра URI переадресации, используется при создании подтверждения DPoP. Токен обновления привязан к подтверждению, которое включается в качестве HTTP-заголовка во все последующие запросы к конечной точке токена.
Чистые одностраничные приложения на стороне клиента, не использующие секреты, не могут напрямую использовать DPoP из-за требования client_secret и ограничений CORS для заголовка DPoP-Nonce. Чтобы защитить одностраничные приложения, направляйте трафик через серверную часть для внешнего интерфейса (BFF), которая действует как конфиденциальный клиент, включает access_type=offline и использует DPoP для привязки токена обновления на стороне сервера.
Как создать подтверждение DPoP
Подтверждение содержит заголовок JOSE и полезную нагрузку.
Чтобы создать заголовок, сгенерируйте пару ключей EC P-256 (ES256) и включите координаты открытого ключа (x и y) в параметр jwk. Также поддерживается пара ключей RSA (RS256), но мы рекомендуем использовать ES256, поскольку у нее меньше размер подписи и ниже вычислительные затраты.
Пример заголовка JOSE:
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "VC91y9ZYdfSWaDv8JaI6gx5ifOw2rn3YdqkAB51Uu6E",
"y": "ikPjOtea4k7fWPVrRYwaA4Ww6iVY3pOOICotHwwGV3o"
}
}
Чтобы создать полезную нагрузку подтверждения, требуются четыре утверждения (htm, htu, iat и jti), а также необязательное утверждение nonce.
Два утверждения, htm: POST и htu: https://oauth2.googleapis.com/token, имеют фиксированные значения и не меняются при отправке запроса к конечной точке токена Google.
Остальные два утверждения (iat и jti) должны генерироваться для каждого запроса. Значение iat – это временная метка выдачи, которая меняется для каждого запроса. Значение утверждения JWT ID (jti) зависит от типа обмена. Когда код авторизации обменивается на токены доступа и токены обновления, значением jti является хеш SHA256 кода авторизации, закодированный в Base-64 и URL, например jti =
BASE64URL(SHA-256(authorization_code)). Утверждение nonce необязательно при первоначальном обмене кодами авторизации и требуется, только если у вашего клиента уже есть действительный DPoP-Nonce из предыдущего запроса.
Пример тела полезной нагрузки:
{
"jti": "o29CN8LIY0l_N8iy5-ilon1guad9NFQHFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"iat": 1784822025
}
Заголовок JOSE и тело полезной нагрузки кодируются и подписываются как JWT (RFC 7519) для непосредственного использования в HTTP-заголовке DPoP в запросе токена. Для ES256 подпись JWS должна использовать необработанный формат IEEE P1363 (R || S, 64 байта для 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"
Токен обновления, связанный с DPoP, возвращается вместе с HTTP-заголовком DPoP-Nonce, например:
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"
}
Всегда кешируйте и отправляйте в последующих запросах токенов последнее значение DPoP-Nonce, возвращенное сервером авторизации Google. Срок действия одноразовых кодов периодически истекает. Если в подтверждении отсутствует обязательное утверждение nonce или содержится недействительный или просроченный одноразовый код, сервер возвращает ответ HTTP 400 (use_dpop_nonce) с новым заголовком DPoP-Nonce для использования при повторных попытках.
Изменения в процессе обновления токенов
Чтобы обновить существующий процесс обновления токена OAuth 2.0, необходимо сгенерировать и отправить подтверждение DPoP в виде HTTP-заголовка, когда токен обновления заменяется новыми токенами, как показано на шагах 2–5 на рисунке 2.
Создание подтверждения DPoP
Метод создания подтверждения для обновления токена отличается от сценария с кодом авторизации. Заголовок JOSE создается так же, как описано выше для запроса кода авторизации. Тело подтверждения создается аналогичным образом, но включает утверждение nonce, а jti содержит уникальную случайную строку.
Чтобы создать тело полезной нагрузки, значение HTTP-заголовка DPoP-Nonce, возвращенное ранее, должно быть включено в утверждение nonce, а временная метка выдачи (iat) должна обновляться для каждого запроса. Идентификатор JWT (jti) – это уникальная случайная строка, которая генерируется для каждого запроса с помощью встроенного WebCrypto API crypto.getRandomValues(new Uint8Array(24)) и кодируется по стандарту Base64URL.
Пример тела полезной нагрузки, содержащего jti, nonce и iat:
{
"jti": "o29CN8ZIY0l_K8iy5-ilon1guad9NF6HFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"nonce": "AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI",
"iat": 1784822025
}
Заголовок JOSE и тело полезной нагрузки закодированы как JWT (RFC7519) для непосредственного использования в HTTP-заголовке DPoP в запросе токена.
Подтверждение добавляется в заголовок DPoP запроса на обновление токена:
$ 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"
Если используется недействительный или просроченный одноразовый код или выполняется переход между разными рабочими процессами OAuth (например, от первоначального обмена кодами авторизации к запросу на обновление токена), сервер Google обеспечивает изоляцию рабочих процессов. Это означает, что сервер безоговорочно отклоняет nonce с запросом HTTP 400 use_dpop_nonce, чтобы установить новое пространство имен nonce для нового рабочего процесса.
Ниже приведен пример ответа 400, который требует повторной попытки и создания нового подтверждения с использованием значения 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."
}
В случае успеха возвращается новый одноразовый код и токен доступа с коротким сроком действия:
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"
}
Сохраните значение DPoP-Nonce, чтобы использовать его в следующем запросе.
Дополнительные сведения и рекомендации можно найти в статьях Использование OAuth 2.0 для веб-серверных приложений и Рекомендации.