Skip to content

mTLS client authentication (RFC 8705)

For high-trust integrations, you can authenticate to the auth service by presenting an X.509 client certificate on the TLS handshake instead of (or in addition to) a client_secret. The auth service verifies the cert against the platform's registered trust store and issues an access token bound to that cert per RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication.

Why use it

  • A leaked client_secret is portable; a leaked key file is harder to exfiltrate and easier to detect.
  • Issued tokens carry a cnf.x5t#S256 claim. Resource servers configured to verify token binding will reject the token if a different cert (or no cert) is presented on the resource call.
  • mTLS is a standard control for procurement / compliance reviews ("HSM-backed key material", "FIPS-validated cipher suites", etc.).

How to opt in

  1. Generate a client key + self-signed cert (or get one issued by your PKI). Example:
    openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:P-256 -days 365 -subj "/CN=acme-corp-production" -keyout client.key -out client.crt
    
  2. Send the certificate (client.crt — not the key) to your platform operator. They upload it through the admin UI's Client certificates (mTLS) card on the integration page.
  3. Update your integration's TLS config to present client.key + client.crt to the auth service's mTLS listener.

Once at least one active certificate is on file, the auth service requires a cert on every token request from this client. Calls without one return 401 mtls_required. There is no fallback to client_secret — that's the whole point.

You can register multiple certs in parallel. Use this for staged rollovers: register the new cert, switch the integration over, then revoke the old one. The trust store is union — any active cert matches.

Token binding

Every token issued via mTLS carries the standard RFC 8705 claim:

{
  "sub": "client-id",
  "org_id": "...",
  "scopes": ["..."],
  "cnf": {
    "x5t#S256": "qrvM3e7_ABEiM0RVZneImaq7zN3u_wARIjNEVWZ3iJk"
  }
}

x5t#S256 is the base64url-encoded SHA-256 of the DER form of the client certificate. Resource servers that participate in mTLS sender-constraint can verify the token holder is the same party that minted it: compute x5t#S256 of the cert presented on the resource call and compare against the claim.

The binding also flows through the urn:ietf:params:oauth:grant-type:token-exchange grant — delegated user tokens minted via mTLS carry the same cnf.

Endpoint configuration

The mTLS endpoint runs on a separate port from the regular HTTP service. In deployed environments:

Setting Default What it does
MTLS_ENABLED false Enables the mTLS HTTPS listener at boot
MTLS_PORT 3443 Port for the mTLS listener
MTLS_TLS_CERT_PATH Server cert PEM (auth service's own TLS cert)
MTLS_TLS_KEY_PATH Server cert key

The listener configures the TLS handshake with requestCert: true, rejectUnauthorized: false — any cert is accepted at the TLS layer and validated by the application using the registered fingerprints. That way a bad cert returns a structured 401 invalid_client instead of a connection drop.

Curl example

curl -X POST https://platform.example.com:3443/v1/auth/oauth/token \
  --cert client.crt --key client.key \
  --data grant_type=client_credentials \
  --data client_id=<your-client-id>
  # no client_secret — cert is the credential

When to also keep a secret

You don't need to. Opting in to mTLS revokes the secret-based path for this client. If you'd like a fallback (mTLS for production, secret for local dev), use two separate OAuth2 clients — one with a cert, one without — and switch which one the deploy uses by environment.

Cert rotation

Certificate rotation is the same shape as secret rotation:

  1. Register the new cert via the admin UI.
  2. Switch your integration to present the new cert.
  3. Revoke the old cert from the admin UI.

There's no overlap-window knob here — both certs are simultaneously valid until you revoke the old one. Tokens minted under the old cert keep working until they expire on their own TTL; if you need immediate invalidation, also rotate the auth service's signing key (out of scope for this doc).