RFC 8705 — Mutual-TLS Client Authentication and Certificate-Bound Access Tokens — Versola Docs
VersolaVersola/docs
0.6.2versola.kzGitHub

RFC 8705 — Mutual-TLS Client Authentication and Certificate-Bound Access Tokens

OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens

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

С mutual TLS клиент аутентифицируется на authorization server сертификатом X.509 вместо общего секрета, а authorization server привязывает выпущенный access token к этому сертификату — украденный токен без соответствующего приватного ключа бесполезен. Механизмы независимы: любой из них работает без второго.

Метаданные регистрации клиента

  • Типы субъекта tls_client_auth — subject_dn, san_dns, san_uri, san_ip, san_email (§2.1.2) — хранятся у клиента одной парой «тип субъекта + значение», потому что сертификат, совпавший хотя бы с одним из них, клиента всё равно аутентифицирует
  • Флаг намерения клиента tls_client_certificate_bound_access_tokens (§3.4)
  • self_signed_tls_client_auth (§2.2) — сертификат аутентифицирует клиента, если его открытый ключ есть в зарегистрированном jwks клиента — том же столбце, что читает private_key_jwt из RFC 7523. Ключи сравниваются по закодированному ключевому материалу, поэтому зарегистрированный ключ с другими необязательными полями (kid, use) всё равно совпадает. Без jwks регистрация с этим методом отклоняется, а зарегистрированные ключи аутентифицируют только сертификат и никогда — client assertion
  • Любой из двух mTLS-методов отклоняется при регистрации, если прокси тенанта не терминирует mutual TLS (см. Терминирование TLS): такой клиент никогда бы не аутентифицировался

Аутентификация клиента

  • tls_client_auth и self_signed_tls_client_auth на token endpoint (§2) во всех трёх grant type: клиент, зарегистрированный для любого из методов, аутентифицируется подходящим сертификатом — и только им. Присланный рядом секрет не проверяется, а отсутствующий или несовпавший сертификат даёт invalid_client
  • Оба метода на introspection, revocation и pushed authorization request endpoint — правило то же, поэтому клиент без секрета может получить токены, а затем читать, отзывать и отправлять pushed request с ними. На introspection дополнительно отклоняется вызов, в котором предъявлен только client_id: RFC 7662 требует аутентифицировать вызывающего, а идентификатор публичного клиента — не секрет

Access token с привязкой к сертификату

  • Claim cnf с членом x5t#S256 в выпускаемых access token (§3.1) — SHA-256 от DER-представления сертификата в base64url; проставляется клиенту, который аутентифицировался сертификатом или зарегистрировал флаг §3.4
  • Refresh token, привязанные к сертификату (§4, §7.1): привязка сохраняется за grant, обновить его можно только тем же сертификатом, и токен продлевается на месте вместо ротации — так же, как токен с привязкой DPoP
  • Обе привязки сразу: токен с привязкой к сертификату, у которого есть ещё и jkt по RFC 9449, сохраняет обе, а не ту из них, которая слабее
  • x5t#S256 в ответах token introspection (§3.2) — /introspect возвращает cnf токена целиком, поэтому resource server, который проверяет токены через introspection, видит отпечаток сертификата вместе с jkt от DPoP, если он есть
  • Проверка привязки на /userinfo (§3) — токен с привязкой к сертификату принимается, только если запрос пришёл с тем сертификатом, к которому он привязан; отсутствующий, нечитаемый или чужой сертификат даёт invalid_token. Проверка следует привязке в самом токене, а не текущей регистрации клиента, поэтому клиент, снявший флаг §3.4, не сможет воспользоваться старым привязанным токеном без сертификата
  • Метаданные authorization server tls_client_certificate_bound_access_tokens (§3.3), а также tls_client_auth / self_signed_tls_client_auth в token_endpoint_auth_methods_supported — публикуются, если оператор задал их в хранимом документе метаданных: документ передаёт их без изменений. Автоматически они не выводятся: документ один на всю инсталляцию, а примет ли она сертификат, зависит от прокси, который настроил каждый тенант
  • mtls_endpoint_aliases (§5) — для тенанта с настроенным mTLS-listener’ом документ дискавери переиздаёт пути token, introspection, revocation, pushed-authorization-request и userinfo endpoint под собственным внешним адресом listener’а, чтобы клиент мог узнать адрес, который действительно терминирует его сертификат. Без такой настройки поле вообще не добавляется — сервис не обещает listener, которого нет

Терминирование TLS (§6.5)

Спецификация выносит это за скобки: «How the client certificate metadata is securely communicated between the intermediary and the application server… is out of scope of this specification». Сервис auth в Versola всегда стоит за обратным прокси, который терминирует TLS, поэтому прежде чем заработает что-либо из перечисленного выше, прокси должен передать в auth проверенный им сертификат — а единого заголовка и единой кодировки для этого у прокси нет.

Реализована именно эта часть: заголовок и кодировка сертификата настраиваются в Central для каждого тенанта.

Настройка

В админке Central, на странице тенанта Challenges & Security, в разделе Mutual TLS:

  1. Отметьте «This tenant’s reverse proxy terminates mutual TLS».
  2. Выберите пресет под свой обратный прокси либо задайте заголовок и кодировку вручную.
ПресетЗаголовокКодировка
nginx / ingress-nginxssl-client-certPEM в URL-кодировке
TraefikX-Forwarded-Tls-Client-CertDER в base64
Customлюбое имя заголовкаPEM в URL-кодировке или DER в base64

Пустая настройка означает, что прокси тенанта mTLS не терминирует, — состояние, в котором находится каждый тенант до того, как её задали.

Заголовок читается только для клиента, на которого сертификат влияет: который аутентифицируется сертификатом, а на /token — ещё и чьи токены к сертификату привязываются. Для остальных он игнорируется, поэтому тенант, чей прокси пересылает сертификат на каждом соединении, не привязывает токены, которые никто не просил привязывать, и не ломает запросы, прекрасно аутентифицирующиеся секретом. Заголовок, который пришёл, но не читается, отклоняется как invalid_client, а не считается отсутствующим сертификатом: причина описывает прокси инсталляции, поэтому она пишется в лог, а не возвращается клиенту.

Известные ограничения

  • Токены с привязкой к сертификату на edge. edge не терминирует клиентский TLS и не читает заголовок с сертификатом, поэтому не может сверить сертификат вызывающего с cnf.x5t#S256 (§3). Вместо этого он принимает такой токен только из собственной cookie EDGE_SESSION — это токен, который edge получил сам как mTLS-клиент, и привязан он к сертификату, который есть только у edge, — и отклоняет его в заголовке Authorization. Значит, mTLS-клиент не может напрямую обратиться к ресурсу за edge со своим токеном, привязанным к его сертификату.