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

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

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

Specification: RFC 8705

Mutual TLS lets a client authenticate to the authorization server with an X.509 certificate instead of a shared secret, and lets the authorization server bind an access token to that certificate so a stolen token is useless without the matching private key. The two mechanisms are independent — a deployment can use either without the other.

Client Registration Metadata

  • tls_client_auth subject types — subject_dn, san_dns, san_uri, san_ip, san_email (§2.1.2) — stored per client as a single discriminated subject type/value pair, since a certificate matching any one of them would authenticate the client anyway
  • tls_client_certificate_bound_access_tokens client intention flag (§3.4)
  • self_signed_tls_client_auth (§2.2) — the certificate authenticates the client when its public key matches one in the client’s registered jwks, the same column RFC 7523 private_key_jwt reads. Keys are compared by encoded key material, so a registered key with different optional members (kid, use) still matches. Registration refuses the method without a jwks, and the registered keys authenticate a certificate only, never a client assertion
  • Either mTLS method is refused at registration for a tenant whose proxy does not terminate mutual TLS (see TLS Termination) — such a client could never authenticate

Client Authentication

  • tls_client_auth and self_signed_tls_client_auth at the token endpoint (§2), across all three grants — a client registered for either method authenticates by presenting a matching certificate, and by nothing else: a secret sent alongside is not consulted, and a missing or non-matching certificate is invalid_client
  • Both methods at the introspection, revocation, and pushed authorization request endpoints — the same rule, so a client that holds no secret can obtain tokens and then read, revoke and push with them. Introspection also refuses a caller presenting nothing but a client_id: RFC 7662 requires the caller to be authenticated, and a public client’s id is not a secret

Certificate-Bound Access Tokens

  • cnf claim with the x5t#S256 member on issued access tokens (§3.1) — the base64url SHA-256 of the DER certificate, set for a client that authenticated with one or registered the §3.4 flag
  • Certificate-bound refresh tokens (§4, §7.1) — a bound grant keeps the binding it was issued with; refreshing it requires the same certificate, and the token is renewed in place rather than rotated, exactly as a DPoP-bound one is
  • Both confirmations at once — a certificate-bound token that also carries an RFC 9449 jkt keeps both, rather than the weaker of the two constraints the client proved
  • x5t#S256 in token introspection responses (§3.2) — /introspect returns the token’s whole cnf, so a resource server that validates tokens by introspection sees the certificate thumbprint alongside any DPoP jkt
  • Enforcing the binding at /userinfo (§3) — a certificate-bound token is honoured only when the request carries the certificate it is bound to; a missing, unreadable, or different certificate is invalid_token. The check follows the binding on the token, not the client’s current registration, so a client that has since dropped the §3.4 flag cannot use an old bound token without its certificate
  • tls_client_certificate_bound_access_tokens authorization server metadata (§3.3), and tls_client_auth / self_signed_tls_client_auth in token_endpoint_auth_methods_supported — served when the operator sets them in the stored metadata document, which passes them through unchanged. They are not derived automatically: the document is served once for the whole deployment, but whether a certificate can be honoured depends on the proxy each tenant configured
  • mtls_endpoint_aliases (§5) — for a tenant with a mutual-TLS listener configured, the discovery document republishes the token, introspection, revocation, pushed-authorization-request, and userinfo endpoint paths under the listener’s own external URL, so a client can discover the address that actually terminates its certificate. Absent that config, the field is omitted entirely — no promise for a listener that doesn’t exist

TLS Termination (§6.5)

The RFC explicitly puts this out of scope: “How the client certificate metadata is securely communicated between the intermediary and the application server… is out of scope of this specification.” Versola’s auth service always sits behind a reverse proxy that terminates TLS, so before anything above this line can work, the proxy first has to hand auth the certificate it validated — and there’s no standard header or encoding for that across proxies.

This is the piece implemented now: a per-tenant certificate header and encoding, configured in Central.

Configuration

In the Central admin UI, on a tenant’s Challenges & Security page, under Mutual TLS:

  1. Check “This tenant’s reverse proxy terminates mutual TLS”.
  2. Pick a preset matching your reverse proxy, or enter a custom header and encoding.
PresetHeaderEncoding
nginx / ingress-nginxssl-client-certURL-encoded PEM
TraefikX-Forwarded-Tls-Client-CertBase64-encoded DER
Customany header nameURL-encoded PEM or Base64-encoded DER

Leaving it unset means the tenant’s proxy does not terminate mTLS — the state every tenant is in before this was configured.

The header is read only for a client the certificate can affect: one that authenticates by certificate, and at /token also one whose tokens are bound to one. For any other client it is ignored, so a tenant whose proxy forwards a certificate on every connection neither constrains tokens nobody asked to constrain nor fails requests that authenticate perfectly well by secret. A header that is present but unreadable is refused as invalid_client rather than treated as an absent certificate — the reason describes the deployment’s proxy, so it is logged instead of returned.

Known Limitations

  • Certificate-bound tokens at edge. edge terminates no client TLS and reads no certificate header, so it cannot check a caller’s certificate against cnf.x5t#S256 (§3). Instead it accepts a certificate-bound token only from its own EDGE_SESSION cookie — a token edge obtained as the mTLS client itself, bound to a certificate only edge holds — and refuses one sent in an Authorization header. A resource behind edge therefore cannot be called directly by an mTLS client with that client’s own certificate-bound token.