feat: add mTLS (RFC 8705) client authentication - #159
Open
cschetan77 wants to merge 30 commits into
Open
Conversation
cschetan77
force-pushed
the
feat/mtls-client-authentication
branch
from
August 21, 2026 09:37
9ef2878 to
0915865
Compare
kishore7snehil
requested changes
Aug 25, 2026
rmad17
requested changes
Aug 26, 2026
…or in docstrings Add missing token_endpoint_override param and ConfigurationError (dpop_key + use_mtls) to MfaClient.verify() docstring; add same ConfigurationError to signin_with_passkey() Raises section.
Replace em dashes with plain hyphens (Rule 4), split semicolon-spliced clauses into separate sentences (Rule 5), and reword the cryptic ssl_context error message in plain direct voice (Rule 9).
Move test_signin_with_passkey_rejects_dpop_under_mtls into the PASSKEY AUTHENTICATION section and test_complete_interactive_login_uses_mtls_token_endpoint into the IPSIE section, next to the other complete_interactive_login tests. The remaining mTLS tests (constructor, resolver, credential-drop, cert-bound warning) stay in the mTLS section.
…ss, and interactive login C22: inject token_endpoint_resolver into MfaClient so mfa.verify() resolves the mTLS alias automatically via ServerClient._resolve_mfa_token_endpoint, keeping metadata fetching and caching in ServerClient where it belongs. C24: add missing null-check on token_endpoint in complete_interactive_login, consistent with the other five call sites. C1: switch passwordless verify from metadata["token_endpoint"] to client._resolve_token_endpoint(metadata) so it routes through the mTLS alias. Credential-drop was already handled by _apply_client_authentication.
The runtime warning flags a resource server misconfiguration the SDK has no control over, using a once-only mechanism inconsistent with the rest of the SDK. MutualTLS.md now states the requirement directly.
…nly configuration
…nnel, connection, and custom-exchange flows
cschetan77
force-pushed
the
feat/mtls-client-authentication
branch
from
September 1, 2026 08:00
0915865 to
82cd04c
Compare
Author
|
All style fixes addressed in one commit (
|
Add _warn_if_not_cert_bound to emit a logger.warning when use_mtls is enabled but the returned access token is not certificate-bound. Called from complete_interactive_login and get_token_by_refresh_token only, matching the grant types covered by RFC 8705 sender-constraining.
Tests distributed next to their surfaces: call-site assertions for complete_interactive_login and get_token_by_refresh_token placed next to their existing mTLS routing tests; unit tests for the method itself in the mTLS section covering warn/no-warn/opaque/missing-token cases.
…sswordless caveat Replace manual openssl thumbprint step with reference to the SDK logger warning. Clarify that MFA challenge/enrollment calls go to the standard host with cert presented in TLS handshake, proxy forwarding outside SDK control. Add passwordless section documenting enforce_client_authentication tenant flag caveat.
Replace token_endpoint_override with token_endpoint_resolver, which is the actual parameter name injected into MfaClient under mTLS.
…tion for mTLS paths
- Replace em dash with plain hyphen in test_mfa_client.py section header - Split semicolon clause-splices into two sentences in MutualTLS.md (x2), server_client.py comment, and MfaVerifyError message - Restructure _resolve_token_endpoint docstring with Returns:/Raises: sections
…call algorithms is a no-op when verify_signature=False - PyJWT only validates alg against the allowlist when performing signature verification.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Mutual TLS (RFC 8705) client authentication to
auth0-server-python. When enabled, the SDK presents a TLS client certificate during the Auth0 token-endpoint handshake instead of a client secret — no credential travels in the request body.ServerClientconstructor params:use_mtls: bool = Falseandssl_context: Optional[ssl.SSLContext] = None. The caller builds theSSLContext(ssl.create_default_context()+load_cert_chain); the SDK forwards it asverify=ssl_contextto everyhttpx.AsyncClientit constructs (including the authlib client used for the authorization-code exchange)._resolve_token_endpoint(metadata)helper returnsmtls_endpoint_aliases.token_endpointfrom the discovery document when mTLS is on, raisingConfigurationErrorif the alias is absent. All token-endpoint call sites inserver_client.py,mfa_client.py, andpasswordless_client.pyare routed through it._resolve_par_endpoint(metadata)helper returnsmtls_endpoint_aliases.pushed_authorization_request_endpointwhen mTLS is on, so PAR requests also hit the mTLS alias._apply_client_authenticationreturnsNoneunder mTLS — the certificate in the TLS handshake is the sole credential; noclient_secretorclient_assertionis added to the body. The authlibAsyncOAuth2Clientis constructed withclient_secret=Noneandverify=ssl_contextunder mTLS._warn_if_not_cert_boundinspects the access token'scnfclaim and emits alogger.warningwhen it is absent, signalling that Token Sender-Constraining is not active on the resource server. Scoped toauthorization_codeandrefresh_tokengrants only (per RFC 8705 Table 1) — does not fire for MFA, passwordless, passkey, connection exchange, or custom token exchange, where the auth server intentionally omitscnf.MfaClientreceivesuse_mtlsandssl_contextfromServerClient(cert on all MFA calls). Atoken_endpoint_resolvercallable is injected at construction byServerClientwhen mTLS is enabled — it calls_resolve_current_domain+_get_oidc_metadata_cached+_resolve_token_endpoint, keeping metadata caching inServerClientwhere it belongs.MfaClientnever fetches OIDC metadata directly./mfa/challengestays on the standard host; the client certificate is presented in the TLS handshake viassl_context./passwordless/startstays on the standard host (no mTLS alias is advertised for it in the discovery doc; mTLS cert auth is not supported server-side on that endpoint).passwordless.verifyroutes the token exchange through the mTLS alias. Behaviour documented inexamples/MutualTLS.md.ssl_contextis threaded throughMyAccountClientso the client certificate is presented on/me/calls, allowing the resource server to verifycnf.x5t#S256binding.signin_with_passkeyandmfa.verifyraiseConfigurationErrorwhen bothdpop_keyanduse_mtlsare active — DPoP would bind the token to its own key and suppresscnf.x5t#S256, silently defeating mTLS token binding./passkey/challengeand/passkey/registerare not served on the mTLS endpoint aliases, so a client usinguse_mtls=True(which forbidsclient_secret) cannot authenticate those endpoints. Documented inexamples/MutualTLS.md.ConfigurationError, fail-fast):use_mtls=Truewithoutssl_context; combined withclient_secret; combined withclient_assertion_signing_key.Changed files
auth_server/server_client.pyuse_mtls/ssl_contextparams, validation,_resolve_token_endpoint,_resolve_par_endpoint,_apply_client_authenticationmTLS branch,_warn_if_not_cert_bound(cnf.x5t#S256 logger warning), DPoP guard, all token/PAR call-site routings,_get_http_client+AsyncOAuth2Clientverify=injectionauth_server/mfa_client.pyuse_mtls/ssl_contextparams,token_endpoint_resolvercallable injection,_get_http_clientverify=injection, DPoP guard inverify()auth_server/passwordless_client.py_resolve_token_endpointauth_server/my_account_client.pyssl_contextthreaded through for cert-bound token support on/me/callstests/test_server_client.py_resolve_token_endpoint,_apply_client_authenticationmTLS branch, SSLContext threading, DPoP exclusion, alias-routing and body credential-absence assertions for interactive login, PAR, refresh token, backchannel, connection, and custom-exchange flows;AsyncOAuth2Clientconstructor verification; cnf warning teststests/test_mfa_client.pytoken_endpoint_resolver, DPoP exclusiontests/test_passwordless_client.pyTestMtlsclass: verify routing through mTLS alias, ssl_context threading onstart()andverify(),client_secretabsence assertionsexamples/MutualTLS.mdREADME.mdreferences/flow-map.mdTest plan
poetry run pytest— all 569 tests passpoetry run ruff check .— no lint errorsConfigurationErrorfor: missingssl_contextwithuse_mtls=True;client_secret+use_mtls;client_assertion_signing_key+use_mtls_resolve_token_endpointreturns the mTLS alias when present, raisesConfigurationErrorwhen alias is absent under mTLS, returns the standard endpoint when mTLS is off_resolve_par_endpointreturns the mTLS PAR alias whenuse_mtls=TrueAsyncOAuth2Clientconstructed withclient_secret=Noneandverify=ssl_contextunder mTLScomplete_interactive_loginunder mTLS hitsmtls_endpoint_aliases.token_endpointget_token_by_refresh_token,backchannel_authentication_grant,get_token_for_connection, andcustom_token_exchangeunder mTLS all route tomtls_endpoint_aliases.token_endpointand omitclient_secretfrom the request bodystart_interactive_loginwith PAR under mTLS hitsmtls_endpoint_aliases.pushed_authorization_request_endpointand omitsclient_secretfrom the request bodypasswordless.verifyunder mTLS routes token exchange through the mTLS alias, omitsclient_secret, and presentsssl_contextpasswordless.startunder mTLS omitsclient_secretand passesverify=ssl_contextto httpx_warn_if_not_cert_boundemitslogger.warningwhencnf.x5t#S256is absent; silent on cert-bound tokens, opaque tokens, and missing access tokencomplete_interactive_loginandget_token_by_refresh_tokenunder mTLS; does not fire for MFA or other non-CNF grantssignin_with_passkeyraisesConfigurationErrorwhendpop_key+use_mtlsmfa.verifyraisesConfigurationErrorwhendpop_key+use_mtls; token endpoint resolved via injectedtoken_endpoint_resolver