Skip to content

feat(mtls): mTLS (RFC 8705) client authentication - #865

Open
jd3vi1 wants to merge 2 commits into
feature/jwefrom
feature/mtls
Open

feat(mtls): mTLS (RFC 8705) client authentication#865
jd3vi1 wants to merge 2 commits into
feature/jwefrom
feature/mtls

Conversation

@jd3vi1

@jd3vi1 jd3vi1 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds mTLS (RFC 8705) client authentication. When enabled, the SDK authenticates to the token endpoint with a TLS client certificate instead of a client secret, and issued access tokens carry a cnf.x5t#S256 claim binding them to the certificate (certificate-bound tokens).

Stacked on #864 (JWE) → #863 (JAR). Review after those merge; the base will retarget accordingly.

What's included

  • useMtls: boolean (or AUTH0_MTLS=true) — single opt-in flag. Internally uses TlsClientAuth() and sets use_mtls_endpoint_aliases: true on the client metadata, so token, refresh, revocation, userinfo, and PAR requests route to the server's mtls_endpoint_aliases. There is no separate CA-signed vs self-signed option; that distinction is an authorization-server concern.
  • customFetch is required with useMtls, and must present the client certificate at the TLS layer (e.g. an undici Agent with connect: { key, cert }). The standard fetch global has no client-certificate API, so the certificate is never configured through the SDK directly.
  • New MtlsError / MtlsErrorCode (exported), thrown at construction/discovery time:
    • mtls_requires_custom_fetchuseMtls without a customFetch.
    • mtls_incompatible_client_authuseMtls combined with clientSecret or clientAssertionSigningKey.
    • mtls_endpoint_aliases_missing — the discovery document does not advertise mtls_endpoint_aliases.token_endpoint (fails fast rather than silently sending unbound requests).
  • A startup warning when useMtls is used against a canonical *.auth0.com issuer, since mTLS requires a custom domain.

Usage

const { Agent, fetch: undiciFetch } = require('undici');

const tlsAgent = new Agent({
  connect: {
    cert: fs.readFileSync('./client.crt'),
    key: fs.readFileSync('./client.key'),
  },
});

app.use(
  auth({
    issuerBaseURL: 'https://auth.your-domain.com', // custom domain
    authorizationParams: { response_type: 'code', audience: 'https://your-api/' },
    useMtls: true,
    customFetch: (url, options) => undiciFetch(url, { ...options, dispatcher: tlsAgent }),
  }),
);

Tests

  • Unit tests for use_mtls_endpoint_aliases metadata, alias surfacing, the alias-missing error, token grants routing to the mTLS alias, and all construction guards (customFetch required, incompatible client auth, AUTH0_MTLS env).
  • Type tests confirming the useMtls/customFetch surface and that mTLS strings are not part of the public clientAuthMethod union.
  • Documentation in EXAMPLES.md and an example at examples/mtls.js.

@jd3vi1
jd3vi1 requested a review from a team as a code owner July 24, 2026 14:00
Comment thread examples/mtls.js Dismissed
@jd3vi1

jd3vi1 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Both majors fixed in 4fc699c:

  1. tls_client_auth removed from the Joi .valid() list. Confirmed your finding that a .default() return is not validated against .valid(), so the useMtls resolver still returns tls_client_auth and mTLS keeps working, while a JS caller passing the string directly (bypassing every useMtls guard) is now rejected.
  2. Explicit clientAuthMethod + useMtls now rejected. Added a guard that throws MTLS_INCOMPATIBLE_CLIENT_AUTH when useMtls is combined with an explicit non-mTLS clientAuthMethod (captured from the raw config before Joi's default resolver fills it), closing the use_mtls_endpoint_aliases + ClientSecretBasic(undefined) contradiction.
  3. Test fixes. Replaced the mislabeled test (it passed self_signed_tls_client_auth, which was never in the list) with an explicit tls_client_auth rejection test, kept a self_signed_tls_client_auth rejection test, and added both incompatible-method cases (client_secret_basic and none).

On the cnf.x5t#S256 debug notice: it is debug()-only output with no assertable side effect and the suite has no debug-capture harness, so I left it untested rather than add brittle plumbing for a minor.

Verified: both fixes exercised end to end; full suite (402), tsd, e2e, and lint all green.

jd3vi1 added 2 commits July 27, 2026 16:28
Authenticate to the token endpoint with a TLS client certificate instead of
a client secret, enabled via useMtls: true (or AUTH0_MTLS=true). Routes token,
refresh, revocation, userinfo, and PAR requests to the server's
mtls_endpoint_aliases, and surfaces the certificate-bound cnf.x5t#S256 claim.

The certificate is presented by a developer-provided customFetch (undici Agent),
never by the SDK. Adds MtlsError and MtlsErrorCode for construction-time guards:
customFetch is required, clientSecret/clientAssertionSigningKey are rejected, and
a missing mtls_endpoint_aliases in discovery is a hard error.
… method

Address review feedback on mTLS:
- Remove 'tls_client_auth' from the clientAuthMethod Joi .valid() list. A JS
  caller could previously set it directly (without useMtls), passing validation
  while every mTLS guard keyed off useMtls stayed inactive -> no customFetch
  required, no endpoint aliases, silent invalid_client at runtime. The useMtls
  default resolver still returns the value (a .default() is not checked against
  .valid()), so mTLS keeps working; only explicit input is rejected.
- Reject useMtls combined with an explicit non-mTLS clientAuthMethod
  (MTLS_INCOMPATIBLE_CLIENT_AUTH), which otherwise produced use_mtls_endpoint_aliases
  metadata with a contradictory auth strategy.
- Fix the mislabeled test (it passed self_signed_tls_client_auth, never a valid
  value) and add explicit tls_client_auth rejection plus both incompatible-method
  cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants