feat(mtls): mTLS (RFC 8705) client authentication - #865
Conversation
|
Both majors fixed in 4fc699c:
On the Verified: both fixes exercised end to end; full suite (402), tsd, e2e, and lint all green. |
|
Restructured: this PR is now independent of #863 (JAR) and branches directly from Heads up on merge order: #863 (JAR) and this PR both touch a few of the same files, so whichever merges second will need a small conflict resolution in |
Piyush-85
left a comment
There was a problem hiding this comment.
Recommend adding token revocation on logout (POST mtls_endpoint_aliases.revocation_endpoint). Without it, refresh tokens remain valid at the authorization server after a user logs out, which is a security concern for certificate-bound sessions. This can land in this PR or a tracked follow-up — if it's deferred, please link the follow-up.
| ); | ||
| } | ||
| } catch { | ||
| // Opaque (non-JWT) access token — cannot inspect for cnf, skip. |
There was a problem hiding this comment.
Minor: JWE access tokens (issued when no audience is set) also throw in decodeJwt and get skipped here, but the Testing Doc expects a warning for the JWE case. Flagging for awareness, not blocking.
There was a problem hiding this comment.
Good catch on the mismatch with the Testing Doc. Rather than add a JWE-specific warning, we've dropped the JWE handling from scope here: JWE access-token decryption is a Resource Server concern (tracked separately, and PR #864 was closed), so this SDK does not inspect encrypted access tokens. Updated the comment wording accordingly in e6cac71 (now reads "Opaque (non-JWT) access tokens ... are skipped", no JWE mention).
|
@Piyush-85 on token revocation on logout: agreed it's a valid concern, but we're deferring it to a separate PR rather than including it here. Two reasons: (1) it isn't mTLS-specific. This repo has never revoked the refresh token on logout for any client-auth method; It doesn't exist anywhere in this repo today, so we'll track it as its own follow-up rather than link an existing one. The other three comments (refresh-path binding check, JWE wording, env-var hint in the error message) are addressed in e6cac71. |
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, and an explicit clientAuthMethod are rejected; and a missing mtls_endpoint_aliases in discovery is a hard error. Warns when used against a canonical *.auth0.com domain.
Address review feedback on the mTLS PR: - Extract the cnf.x5t#S256 binding check into warnIfNotCertificateBound and call it from both the callback and refresh() paths, so a token that loses its certificate binding on refresh also warns. - Mention the CLIENT_SECRET environment variable in the MTLS_INCOMPATIBLE_CLIENT_AUTH message to speed up debugging when the secret was seeded from the environment. - Drop the JWE/encrypted-token wording; JWE access tokens are out of scope for this SDK.
Address review feedback on the mTLS PR: - Make endpoint-alias validation alias-aware for every endpoint the config will use, not just token_endpoint. oauth4webapi resolves mtls_endpoint_aliases per endpoint and silently falls back to the standard (non-mTLS) host when an alias is absent, so a missing alias would send that request over a channel that cannot extract the client certificate. Throw at construction when the token alias (always used) or the PAR alias (when pushedAuthorizationRequests is enabled) is missing, and debug-log a missing userinfo/revocation alias. - Make the PAR precondition alias-aware so a server advertising PAR only under mtls_endpoint_aliases is no longer wrongly rejected at construction. - Raise the "token is not certificate-bound" warning from debug() to console.warn(), matching the custom-domain hint, since an unbound token silently defeats sender-constraining. Log at debug() in the decode catch so a genuine JWT decode failure stays distinguishable from an opaque token. - Type MtlsError.code (and the constructor) to the MtlsErrorCode union so consumers can narrow on it in a switch/if. - Normalize AUTH0_MTLS parsing to accept case and surrounding whitespace. - Resolve the example's certificate paths relative to __dirname and fail with a message naming the expected files instead of an opaque ENOENT at import. - Reword the EXAMPLES.md routing claim to state aliases are used when advertised, and that a missing alias means a non-mTLS channel. - Route the mTLS routing test through getClient (exercising the SDK's own flag-setting and getClientAuth branch) instead of hand-building a Configuration, assert on .code/.name rather than instanceof, and add coverage for the new PAR alias guards. - Revert an unrelated prettier reflow in BackchannelLogoutOptions.
… tests - Warn when useMtls is set but response_type lacks "code": mTLS applies to the token endpoint, which is only reached in the code flow, so an implicit-only response_type leaves the entire mTLS setup inert. - resolveEndpoint now treats a non-string discovery value as absent so the precondition checks fail fast instead of passing on a truthy non-URL that oauth4webapi would later reject as INVALID_SERVER_METADATA. - Pin the tls_client_auth / self_signed_tls_client_auth rejection tests to the Joi .valid() failure (TypeError + message); the self_signed case was previously passing on an unrelated public-client error under a code flow.
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#S256claim binding them to the certificate (certificate-bound tokens).Independent of #863 (JAR); both branch from
master.What's included
useMtls: boolean(orAUTH0_MTLS=true) — single opt-in. Internally usesTlsClientAuth()and setsuse_mtls_endpoint_aliases: trueon the client metadata, so token, refresh, revocation, userinfo, and PAR requests route to the server'smtls_endpoint_aliases. There is no separate CA-signed vs self-signed option; that distinction is an authorization-server concern.customFetchis required withuseMtls, and must present the client certificate at the TLS layer (e.g. anundiciAgentwithconnect: { key, cert }). The certificate is never configured through the SDK directly.MtlsError/MtlsErrorCode(exported), thrown at construction / discovery time:mtls_requires_custom_fetch—useMtlswithout acustomFetch.mtls_incompatible_client_auth—useMtlscombined withclientSecret,clientAssertionSigningKey, or an explicitclientAuthMethod.mtls_endpoint_aliases_missing— the discovery document does not advertisemtls_endpoint_aliases.token_endpoint(fails fast rather than a silent runtimeinvalid_client).useMtlsis used against a canonical*.auth0.comissuer, since mTLS requires a custom domain.Usage
Tests
use_mtls_endpoint_aliasesmetadata, aliases surfaced in server metadata, the alias-missingMtlsError, token grants routing to the mTLS alias, and all construction guards (customFetchrequired, incompatible client auth including an explicitclientAuthMethod,AUTH0_MTLSenv, rejection of thetls_client_authstring).useMtls/customFetchsurface and theMtlsError/MtlsErrorCodeshape.cnf.x5t#S256matching the client certificate thumbprint, including across refresh.EXAMPLES.mdand an example atexamples/mtls.js.