RFC 9449 — Demonstrating Proof of Possession (DPoP) — Versola Docs
/docs
До 1.00.7.5versola.ioGitHub

RFC 9449 — Demonstrating Proof of Possession (DPoP)

OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer

Спецификация: RFC 9449

DPoP привязывает access token — а если клиент запросил offline access, то и выданный вместе с ним refresh token — к паре ключей, которую держит клиент, так что перехваченный без приватного ключа токен нельзя воспроизвести от чужого имени. auth выпускает и проверяет proof’ы на token endpoint и /userinfo; edge, компонент resource server перед проксируемыми API, применяет полный набор проверок — включая nonce, если у edge включена настройка Require DPoP nonce — на каждый проксируемый запрос.

Статус

Реализован. Отмечено пунктов: 37 из 37.

Валидация Proof JWT

  • Заголовок typ: dpop+jwt
  • Allowlist алгоритмов подписи — берется из dpop_signing_alg_values_supported в документе метаданных authorization server, так что набор, который узнают клиенты, и набор, по которому проверяется proof, — одно и то же значение. ES256 и PS256, если документ о них молчит; для RS256 верификатор есть, но в этот набор он не входит (FAPI 2.0 его запрещает, так что деплоймент, которому он нужен, должен назвать его явно)
  • Публичный ключ передается в заголовке jwk самого proof’а и проверяется против ключа, который он несет — а не через kid, указывающий на ключ, выпущенный этим сервером
  • Проверка подписи против этого встроенного ключа
  • htm — привязка к HTTP-методу текущего запроса
  • htu — привязка к URI текущего запроса, query и fragment отбрасываются перед сравнением. URI строится из настроенного issuer, а не из заголовка Host, и proof, отправленный на listener mutual TLS, сравнивается с адресом самого listener (MutualTlsListenerSpec)
  • iat — проверяется с настраиваемым допуском (dpop.iat-leeway, по умолчанию 60с), симметрично в обе стороны
  • jti — обязателен, используется для проверки одноразовости ниже

Token Endpoint

  • DPoP опционален для каждого запроса — запрос без заголовка DPoP все равно выдает обычный bearer token
  • Требуется ровно один заголовок DPoP — больше одного отклоняется до разбора любого proof’а
  • cnf.jkt — thumbprint ключа, которым доказывается владение, привязывается к выпущенному access token
  • token_type отражает привязку — DPoP, если присутствует cnf.jkt, иначе Bearer
  • error="use_dpop_nonce" со свежим заголовком ответа DPoP-Nonce на отказе — чтобы клиент повторил запрос уже с ним
  • Обязательный со стороны сервера nonce — настройка тенанта Require DPoP nonce (requireDpopNonce, по умолчанию выключена) делает nonce обязательным для каждого proof’а, который проверяет этот endpoint. Nonce живет dpop.nonce-ttl (по умолчанию 300 с). По умолчанию выключено: каждому клиенту это стоит лишнего круга, а перехваченный proof на token endpoint дает атакующему немного

Resource Endpoints (/userinfo)

  • ath — proof должен быть привязан к реально предъявленному access token (base64url(sha256(token)))
  • Совпадение cnf.jkt — proof, подписанный любым ключом, кроме того, к которому токен был привязан при выпуске, отклоняется
  • Отказ в downgrade до Bearer — DPoP-bound токен, предъявленный по схеме Bearer, отклоняется полностью согласно §7.2, а не принимается молча
  • WWW-Authenticate: DPoP error="invalid_dpop_proof", и error="use_dpop_nonce" с заголовком DPoP-Nonce — на соответствующих ошибках
  • Требуется ровно один заголовок DPoP, как и на token endpoint
  • Обязательный со стороны сервера nonce — та же настройка тенанта, действует и здесь

Edge на /userinfo (расширение Versola, не часть RFC 9449)

edge сам терминирует DPoP для тех же bound-токенов (см. ниже) и отдельно вызывает /userinfo от имени пользователя, чтобы построить свой authorization context. Приложить к этому вызову proof клиента он не может: proof адресован edge, а приватный ключ клиента никогда не покидает клиента. Непроверяемое предъявление bound-токена по Bearer auth для этого не принимает. Он принимает вызов только на mutual-TLS listener’е, от сертификата, в котором назван ровно один edge (urn:versola:edge:<id>) и который подписан сертификатом из mutual-tls.edge-trusted-certificates, и только для этого вызова пропускает проверку proof. Токен, привязанный и к ключу, и к сертификату, отклоняется и здесь, а сертификат в пробрасываемом заголовке никогда не считается сертификатом edge. См. Токены с привязкой к отправителю.

Edge Resource Server (проксируемые вызовы API)

  • Привязка ath и cnf.jkt, те же проверки, что и на /userinfo
  • Nonce обязателен на каждом proof, если в central у edge включено Require DPoP nonce (§9, §11.3); когда настройка выключена, nonce не выдается и не проверяется, поэтому повтор без nonce не обходит challenge
  • Требуется ровно один заголовок DPoP
  • htu пересобирается из конфигурации, а не берется из заголовка Host самого запроса

Защита от Replay

  • Одноразовость — пара (jkt, jti) proof’а записывается при первом использовании, повтор отклоняется
  • Между репликами — общее, UNLOGGED, партиционированное кольцо в Postgres, а не in-process кэш, поэтому proof, повторно использованный на другом инстансе внутри окна iat, будет пойман, а не только повтор на том же инстансе
  • Вытеснение по слотам — proof’ы раскладываются по временным слотам фиксированной ширины на основе собственного iat, и весь слот усекается целиком, как только в нем не может остаться ничего допустимого, вместо построчного удаления
  • edge дополнительно держит локальное in-memory кольцо перед общим — проверяется первым, к общему хранилищу обращаются только при локальном промахе — и деградирует, а не блокирует, если общее хранилище недоступно, откатываясь на защиту только локальным кольцом вместо отказа в каждом проксируемом вызове

Привязка Refresh Token

  • Refresh token, выпущенный вместе с DPoP-bound access token, сам sender-constrained — последующий refresh grant должен предъявить proof для того же ключа
  • Несовпадение ключа при refresh — invalid_grant, а не молчаливая перепривязка к любому предъявленному ключу
  • Bound refresh token не ротируется — токену, который доказуемо все еще находится у той же стороны, которой он был выдан, ротация ничего не дает, поэтому для него не ведутся цепочка ротации, ее idempotency key и состояние повтора
  • Introspection отражает привязку — token_type: "DPoP" и cnf.jkt в ответе introspection для bound-токена

Метаданные Authorization Server

  • dpop_signing_alg_values_supported — публикуется, и это единственное место, где принимаемый набор зафиксирован: auth берет алгоритмы, по которым проверяет proof, прямо из выдаваемого документа, а не из своего конфига, так что расхождение между ними невозможно. Алгоритм, названный там, но для которого у auth нет верификатора, отбрасывается, а не публикуется: документ не обещает того, за что proof потом будет отклонен

Метаданные клиента

  • dpop_bound_access_tokens — клиент, зарегистрированный с этим флагом, всегда использует DPoP: запрос токена от него без proof’а отклоняется с invalid_dpop_proof при каждом grant, а не отвечается bearer-токеном. По умолчанию выключен, так что клиент, не запросивший этого, по-прежнему решает на уровне каждого запроса

Привязка Authorization Code

  • Параметр authorization request dpop_jkt (§10) — клиент фиксирует на этапе authorization request ключ, против которого будет обменян его code, и code несет этот thumbprint через весь flow логина. Обмен с proof’ом для любого другого ключа, или вовсе без proof’а, отклоняется с invalid_grant, при этом code остается неизрасходованным, чтобы зафиксировавший его клиент все еще мог им воспользоваться. Значение, не являющееся base64url SHA-256 thumbprint’ом, отклоняется прямо на /authorize, где клиенту еще можно объяснить причину. Тот же парсер обслуживает /par, так что pushed request фиксирует ключ точно так же

Политика ключей клиента

  • Клиент может сузить допустимые алгоритмы (dpopSigningAlgs) и поднять нижнюю границу RSA-ключа (dpopMinRsaKeySize); RSA-ключи короче 2048 бит отклоняются для любого клиента (DpopKeyPolicySpec)
  • Сужение политики не ломает токены, уже привязанные при более широкой политике (DpopKeyPolicySpec)

Примечания

  • DPoP на /token включается по запросу, если клиент не зарегистрирован с dpop_bound_access_tokens; такой флаг обязан поставить клиент fapi2 без сертификата. См. токены с привязкой к отправителю.
  • edge отклоняет токен, привязанный и к ключу DPoP, и к сертификату, при любом предъявлении.

Куда смотреть

Токены с привязкой к отправителю объясняют DPoP и mTLS, а конфигурация Edge — блок dpop.