Руководство по внедрению DPoP

В этом руководстве подробно описано, как реализовать DPoP (Demonstrating Proof-of-Possession) в ваших интеграциях OAuth 2.0 с платформой OAuth от Google. DPoP (определенный в RFC 9449 ) защищает ваши приложения от кражи токенов и атак повторного воспроизведения путем криптографической привязки токенов к сгенерированной клиентом асимметричной паре ключей.

Изменения в потоке кода авторизации

Добавление DPoP к существующему потоку авторизации OAuth 2.0 требует генерации и хранения пары ключей, создания JWT-доказательства DPoP и включения этого доказательства в заголовок HTTP при обмене кода авторизации на токен обновления, как показано на шагах 5 и 6 рисунка 1.

Поток авторизационных кодов с DPoP
Рисунок 1. Последовательность событий в процессе ввода кода авторизации с использованием DPoP.

Запрос кода авторизации

Запрос на авторизацию создается в обычном режиме. Например:

$ 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 во всех последующих запросах к конечной точке токена.

Чистые, не содержащие секретов одностраничные приложения (SPA) на стороне клиента не могут напрямую использовать DPoP из-за требования client_secret и ограничений CORS в заголовке DPoP-Nonce . Для обеспечения безопасности SPA трафик следует направлять через бэкэнд-фор-фронтенд (BFF), который выступает в роли конфиденциального клиента, включает access_type=offline и использует DPoP для привязки токена обновления на стороне сервера.

Создайте доказательство DPoP.

Доказательство содержит заголовок JOSE и полезную нагрузку.

Для создания заголовка необходимо сгенерировать пару ключей EC P-256 (ES256) и указать координаты открытого ключа ( x и y ) в параметре jwk . Также возможна пара ключей RSA, но она не рекомендуется из-за более высоких вычислительных затрат.

Это пример заголовка JOSE:

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

Для построения подтверждающей полезной нагрузки требуется четыре значения.

Два параметра: 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)) .

Это пример содержимого полезной нагрузки:

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

Заголовок JOSE и тело полезной нагрузки закодированы в формате JWT (RFC7519) для непосредственного использования в заголовке HTTP DPoP в запросе токена:

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

Вместе с HTTP-заголовком DPoP-Nonce возвращается привязанный к DPoP токен обновления, например:

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

Сгенерированный сервером авторизации Google одноразовый код (nonce) должен быть включен в каждый последующий запрос токена. Обратите внимание, что значение nonce используется только один раз, а отсутствующее, недействительное, просроченное или повторно используемое значение nonce отклоняется с ответом HTTP 400 В этом случае возвращается новый nonce для использования при повторных попытках.

Изменения в процессе обновления токенов

Для обновления существующего процесса обновления токенов OAuth 2.0 необходимо сгенерировать и отправить подтверждение DPoP в качестве заголовка HTTP при обмене токена обновления на новые токены, как показано на шагах 2-5 рисунка 2.

Процесс обновления токенов с использованием DPoP
Рисунок 2. Последовательность событий в процессе обновления токена с обработкой ошибок и повторной попыткой.

Создание доказательства DPoP

Метод построения подтверждения для обновления токена отличается от сценария с кодом авторизации. Заголовок JOSE строится так же, как описано ранее при построении запроса на код авторизации. Тело подтверждения строится аналогично, но включает в себя утверждение nonce , а jti содержит уникальную случайную строку.

Для формирования тела полезной нагрузки необходимо включить ранее возвращенное значение заголовка HTTP DPoP-Nonce в утверждение nonce , а метку времени issued-at ( iat ) обновлять для каждого запроса. Идентификатор JWT ( jti ) представляет собой уникальную случайную строку, генерируемую для каждого запроса с использованием встроенного API WebCrypto crypto.getRandomValues(new Uint8Array(24)) и кодирования строки в Base64URL.

Это пример тела полезной нагрузки, содержащего jti , nonce и iat :

{
  "jti": "o29CN8ZIY0l_K8iy5-ilon1gwad9NF6HFOdXTzrBNck",
  "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: 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"

Когда используется просроченный, некорректный или повторно используемый nonce, или при переходе между различными рабочими процессами 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."
}

В случае успеха возвращаются новый одноразовый код (nonce) и кратковременный токен доступа:

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

Сохраните значение DPoP-Nonce для использования в следующем запросе.

Дополнительные сведения и рекомендации см. в разделе «Использование OAuth 2.0 для веб-серверных приложений и лучшие практики» .