RFC 8705 — Mutual-TLS Client Authentication and Certificate-Bound Access Tokens — Versola Docs
/docs
Pre-1.00.7.5versola.ioGitHub

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.

Status

Implemented. Checked claims: 17 of 17.

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), in 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

Mutual TLS listener

  • A separate auth listener (port 8083 by default) that demands a client certificate in the handshake; a connection without one is refused (MutualTlsListenerSpec)
  • The listener serves only the endpoints a certificate matters for: /token, /introspect, /revoke, /par and /userinfo
  • Enabled by the mutual-tls block of the auth configuration, with certificate, private-key, trusted-certificates and external-url. trusted-certificates is required, and there is no fallback to the JDK trust store
  • A DPoP proof sent to the listener is held to the listener’s own address, and a proof made for the main address is refused there (MutualTlsListenerSpec)

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 between proxies.

This is the second way to receive a certificate, next to the listener above: a per-tenant certificate header and encoding, configured in Central.

Configuration

In the Central admin UI, on a tenant’s Security page, in the Tokens & keys card, 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.

Notes

  • Certificate-bound tokens at edge. The plain edge port terminates no client TLS and reads no certificate header, so it cannot check a caller’s certificate against cnf.x5t#S256 (§3). There it accepts a certificate-bound token only from its own EDGE_SESSION cookie (a token edge obtained as the mTLS client itself) and refuses one sent in an Authorization header (certificate_bound_token_outside_session). A caller that holds its own certificate-bound token calls the API on edge’s optional mutual-TLS listener instead: edge reads the certificate from the handshake and accepts the token only if its SHA-256 thumbprint equals cnf.x5t#S256.

  • Renewed certificates and tokens. A token carries the thumbprint of the certificate it was issued for. After the client renews its certificate (for example through EST), a token bound to the old certificate does not validate with the new one: the client keeps presenting the old certificate with the old tokens until they expire, or requests new tokens with the new certificate.

Where to look

Sender-constrained tokens explains certificate binding, and Edge client certificates covers the certificates edge presents.