Repository navigation
feat: add mTLS (RFC 8705) client authentication #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
f970913
feat: add use_mtls and ssl_context constructor args with validation
cschetan77 41718f5
feat: pass mTLS ssl_context to httpx and authlib clients
cschetan77 4b6a987
feat: add _resolve_token_endpoint mTLS alias resolver
cschetan77 2dc6158
feat: return no body credential under mTLS in client auth resolver
cschetan77 f3602ff
feat: route all token-endpoint calls through mTLS alias resolver
cschetan77 1feaa12
feat: reject dpop_key + use_mtls in signin_with_passkey
cschetan77 9ce1c86
feat: warn when mTLS token lacks cnf.x5t#S256 binding
cschetan77 eca5df7
feat: thread mTLS ssl_context and alias routing through MFA verify
cschetan77 5c0aad7
docs: document mTLS client authentication
cschetan77 929942c
docs: document token_endpoint_override and dpop+mTLS ConfigurationErr…
cschetan77 961dd5f
style: apply repo conventions to mTLS code and docs
cschetan77 8f30ff4
refactor(tests): distribute mTLS tests next to their surfaces
cschetan77 11210d8
docs: link to Auth0 mTLS configuration docs in README
cschetan77 7089ab2
fix: route mTLS token calls through alias resolver in MFA, passwordle…
cschetan77 9f7bcb6
refactor: replace cnf.x5t#S256 UserWarning with documentation
cschetan77 be36714
docs: document passkey challenge/register incompatibility with mTLS-o…
cschetan77 b266f27
feat: wire ssl_context through MyAccountClient for mTLS cert-bound to…
cschetan77 eeed1e0
fix: route PAR endpoint through mTLS alias when use_mtls is enabled
cschetan77 3545eeb
test: add mTLS token endpoint routing assertions for refresh, backcha…
cschetan77 f6df221
docs: document use_mtls and ssl_context in ServerClient.__init__ docs…
cschetan77 cc7cab1
feat: log warning when mTLS access token lacks cnf.x5t#S256 claim
cschetan77 6d78587
test: add cert-bound warning tests for _warn_if_not_cert_bound
cschetan77 6a071de
docs: update MutualTLS.md with logger warning, MFA proxy note, and pa…
cschetan77 05a26c6
docs: fix stale token_endpoint_override reference in flow-map
cschetan77 d0a38f2
test: add mTLS routing, credential, and ssl_context assertions for pa…
cschetan77 8b05915
test: add body credential assertions and authlib constructor verifica…
cschetan77 3035b66
style: fix repo convention violations flagged in PR review
cschetan77 37dba93
refactor: remove redundant algorithms arg from unverified jwt.decode …
cschetan77 b6e5096
style: fix semicolon clause-splice in passwordless verify error message
cschetan77 eccff25
docs: fix passkeys section and remove what-comment in server_client
cschetan77 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # Mutual TLS (mTLS) Client Authentication | ||
|
|
||
| Authenticate to Auth0 with a TLS client certificate instead of a client secret (RFC 8705). The certificate is presented during the TLS handshake. No credential travels in the request body. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Auth0 **Enterprise** tenant with the **Highly Regulated Identity** add-on | ||
| - A `self_managed_certs` **custom domain** configured on the tenant | ||
| - **Allow mTLS Endpoint Aliases** enabled on the tenant (Dashboard → Settings → Advanced) | ||
| - Client application's authentication method set to **mTLS** in Dashboard → Applications → Settings → Credentials | ||
|
|
||
| ## Generating a client certificate (development) | ||
|
|
||
| ```bash | ||
| # Self-signed CA + client cert (development only - use your PKI in production) | ||
| openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt -days 365 -nodes \ | ||
| -subj "/CN=dev-ca" | ||
| openssl req -newkey rsa:2048 -keyout client.key -out client.csr -nodes \ | ||
| -subj "/CN=my-app-client" | ||
| openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ | ||
| -out client.crt -days 365 | ||
| ``` | ||
|
|
||
| ## Wiring into `ServerClient` | ||
|
|
||
| ```python | ||
| import ssl | ||
| from auth0_server_python.auth_server.server_client import ServerClient | ||
|
|
||
| ssl_context = ssl.create_default_context() # trusts system/public CAs for the server side | ||
| ssl_context.load_cert_chain("client.crt", "client.key") # attaches the client identity | ||
|
|
||
| auth0 = ServerClient( | ||
| domain="login.example.com", # self_managed_certs custom domain | ||
| client_id="<AUTH0_CLIENT_ID>", | ||
| use_mtls=True, | ||
| ssl_context=ssl_context, | ||
| secret="<AUTH0_SECRET>", | ||
| authorization_params={ | ||
| "audience": "<API_IDENTIFIER>", | ||
| "scope": "openid profile email offline_access", | ||
| }, | ||
| ) | ||
| ``` | ||
|
|
||
| The SDK passes `ssl_context` as `verify=ssl_context` to every `httpx.AsyncClient` it constructs, including the authlib client used for the authorization-code exchange. You never call `load_cert_chain` inside the SDK - the caller owns the TLS material. | ||
|
|
||
| ## Mutual exclusion | ||
|
|
||
| `use_mtls=True` cannot be combined with: | ||
|
|
||
| | Parameter | Reason | | ||
| |-----------|--------| | ||
| | `client_secret` | One client-auth method only - Auth0 rejects requests carrying both. | | ||
| | `client_assertion_signing_key` | Same - one method only. | | ||
| | `dpop_key` (per-call on `signin_with_passkey` / `mfa.verify`) | DPoP binds to its own key (`cnf.jkt`) and suppresses `cnf.x5t#S256`. Combining them silently defeats mTLS token binding. | | ||
|
|
||
| All three raise `ConfigurationError` immediately (constructor for the first two, at the call site for DPoP). | ||
|
cschetan77 marked this conversation as resolved.
|
||
|
|
||
| ## Token sender-constraining | ||
|
|
||
| When the target API has **Token Sender-Constraining (mTLS)** enabled, issued access tokens carry a `cnf.x5t#S256` claim binding the token to the certificate thumbprint. | ||
|
|
||
| The SDK logs a warning at the `auth0_server_python.auth_server.server_client` logger whenever an access token returned by the authorization-code or refresh-token flow does not contain `cnf.x5t#S256`. If you see that warning, enable **Token Sender-Constraining (mTLS)** on the API resource server in the Auth0 Dashboard. | ||
|
|
||
| ## MFA under mTLS | ||
|
|
||
| The client certificate is presented on all MFA API calls. The token-endpoint call inside `mfa.verify` is routed through the mTLS alias automatically. Challenge and enrollment calls (`/mfa/challenge`, `/mfa/associate`) go to the standard host. The certificate is still included in the TLS handshake, but whether it reaches the Auth0 backend depends on the proxy configuration. | ||
|
|
||
| ```python | ||
| await auth0.mfa.verify( | ||
| {"mfa_token": encrypted_token, "otp": "123456"}, | ||
| ) | ||
| ``` | ||
|
|
||
| ## Passkeys under mTLS | ||
|
|
||
| The client certificate is presented on all passkey calls. The token-exchange step (`signin_with_passkey`) calls the token endpoint, which is served on the mTLS alias and routed correctly. | ||
|
|
||
| Challenge and enrollment calls (`/passkey/challenge`, `/passkey/register`) go to the standard host - they are not listed in `mtls_endpoint_aliases`. The certificate is still included in the TLS handshake, but whether it reaches the Auth0 backend depends on the proxy configuration - the same behaviour as `/mfa/challenge`. | ||
|
|
||
| ## Passwordless under mTLS | ||
|
|
||
| By default, Auth0 does not require client authentication on `/passwordless/start`. If the `enforce_client_authentication_on_passwordless_start` tenant flag is enabled on your tenant, the call will fail because `/passwordless/start` does not support certificate-based client authentication. | ||
|
|
||
| The verify step (`passwordless_client.verify`) calls the token endpoint, which is served on the mTLS alias and routed correctly. | ||
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.