Repository navigation
Add SNI certificate support over mTLS (Bearer-over-mTLS) - #947
Open
Robbie-Microsoft wants to merge 12 commits into
Open
Robbie-Microsoft wants to merge 12 commits into
Robbie-Microsoft wants to merge 12 commits into
Conversation
Introduce an app-level opt-in `send_certificate_over_mtls` on the confidential-client `client_credential` dict. When set, MSAL presents the SN/I certificate on the TLS handshake to the [region.]mtlsauth.* endpoint and signs a private_key_jwt client_assertion (x5c forced), but requests a plain Bearer token (no token_type=mtls_pop / no key_id / no req_cnf). The result is an ordinary, non-cert-bound Bearer access token. - Validate the flag at construction: it requires a certificate credential, else ValueError naming the flag. - Add _get_mtls_bearer_client(): reuses the Task-1 mTLS transport (msal/mtls.py) + the login.*->[region.]mtlsauth.* endpoint transform, adds a signed x5c-forced assertion, and caches a plain (non-key_id-fenced) token. - Route acquire_token_for_client() to it when the flag is set and the request is not mtls_pop. A per-request mtls_proof_of_possession=True always wins. - Tests (RED-first) in TestSendCertificateOverMtls: config/builder (default false, stored true, flag + non-cert credential raises) and CC behavior (global/regional mtlsauth endpoint, client_assertion + jwt-bearer, x5c forced, no PoP wire markers, plain-Bearer result, mtls_pop precedence). Ports MSAL.NET CertificateOptions.SendCertificateOverMtls (client-credentials leg). OBO/refresh/auth-code coverage follows. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Extend the app-level send_certificate_over_mtls opt-in beyond client-credentials to every confidential-client user flow: on-behalf-of, refresh-token, and authorization-code. Add _client_for_confidential_request(): when the flag is set it returns the Bearer-over-mTLS client (SN/I cert on the TLS handshake, routed to the [region.]mtlsauth.microsoft.com endpoint, private_key_jwt client_assertion with the x5c chain forced, plain Bearer token back), otherwise the regular client. Swap it in at the three user-flow call sites. Client-credentials routing stays in _acquire_token_for_client, where a per-request mtls_proof_of_possession still wins over the app-level flag. Tests (tests/test_application.py, TestSendCertificateOverMtls): OBO / refresh / auth-code with the flag hit the global mtlsauth endpoint, carry client_assertion + client_assertion_type=jwt-bearer with no key_id/req_cnf, and use the correct grant (on_behalf_of / refresh_token / authorization_code); OBO with the flag + a configured region hits the regional mtlsauth host; OBO without the flag stays on the regular login endpoint (negative control). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Guard the .NET MtlsBearerUserFlowTests second-call regression, adapted to msal-python's cache seam: a 2nd Bearer-over-mTLS client-credentials call is served from cache with no 2nd network round-trip and no crash on region/instance-metadata resolution, returning the cached plain Bearer AT (token_type=Bearer, NOT cert-bound). In .NET the 2nd call crashes because the AT is cached under the mtlsauth.* host and instance-metadata discovery rejects that host. msal-python structurally avoids this by decoupling the cache/metadata authority (login.*, where the plain Bearer AT lives) from the transport endpoint (mtlsauth.*): the 2nd lookup resolves login.* -- the normal, always-safe path -- so the cached Bearer AT is returned without feeding the mtlsauth.* host into discovery. Only client-credentials auto-serves from cache in msal-python (OBO, refresh, and auth-code have no silent-first lookup), so CC is the flow that exercises this regression. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cover the send_certificate_over_mtls feature in tests/test_e2e.py, mirroring MSAL .NET's live + capture split and reusing the SNI allow-listed app/authority constants already in PublicCloudScenariosTestCase. - test_credential_x509_send_certificate_over_mtls_output_bearer: LIVE client-credentials acquisition (allow-listed app, westus3, Key Vault resource) that presents the SN/I cert on the TLS handshake to the mTLS endpoint and asserts a PLAIN Bearer token back (token_type=Bearer, binding_certificate absent) plus a normal flagless cache hit -- the caller-visible proof that the AT is an ordinary Bearer entry, not a key-id-fenced mtls_pop entry. - test_send_certificate_over_mtls_user_flows_capture_request: request-capture for OBO, refresh-token, and auth-code over the real lab SN/I certificate -- asserts each builds the mtlsauth endpoint + a private_key_jwt client_assertion + the flow's own grant with none of the mtls_pop markers, without needing those (non-mTLS-enabled) apps allow-listed. - test_send_certificate_over_mtls_user_flows_live_acquire: skip-gated live OBO/refresh placeholder documenting the pending server-side app mTLS-enablement (AADSTS700027 / AADSTS392189), mirroring MSAL .NET's [Ignore]. All three are gated (lab cert / explicit skip), collect cleanly, and skip locally; the live and capture cells run in the lab pipeline. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a docstring admonition to the client_credential parameter describing the Bearer-over-mTLS opt-in: set send_certificate_over_mtls=True on the certificate credential dictionary to present the cert on the mTLS handshake and receive a plain (non-cert-bound) Bearer token. Notes that it covers all four confidential flows, defaults to False, requires a certificate credential, and that a per-request mtls_proof_of_possession=True always takes precedence. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a negative assertion to test_cc_second_call_is_bearer_cache_hit that the cached plain Bearer AT is stored under the ORIGINAL login.* host and its environment never contains "mtlsauth". This is the assertion that would fail if the cache seam were ever switched to the .NET-style mtlsauth.* environment (Option A), which would silently break msal-python's login.*-keyed cache lookup and defeat the 2nd-call cache hit. Also assert result1 == result2 (the same cached AT is returned) alongside the existing exactly-one-network-POST guard. Mirrors the cross-SDK finding that a 2nd-call cache test can pass without actually proving the cache environment; this makes the env dependence explicit and deterministic. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved transport and certificate-chain issues, combined with authentication and persistent-cache changes, require fixes and final human review.
2 open issues
What changed in this PR
Adds opt-in Bearer-over-mTLS authentication to MSAL’s confidential-client flows, distinct from certificate-bound mTLS PoP tokens.
Changes:
- Adds certificate validation, forced
x5cassertions, and mTLS routing for client-credentials and user flows. - Updates certificate-chain handling, TLS trust configuration, and bound-token cache isolation.
- Expands regression and live-test coverage; live user-flow acquisition remains gated.
| File | Description |
|---|---|
| tests/test_token_cache.py | Tests key normalization, collision prevention, and legacy-entry handling. |
| tests/test_mtls_transport.py | Tests certificate chains, TLS handshakes, and CA-bundle handling. |
| tests/test_e2e.py | Adds live client-credentials coverage and user-flow request capture. |
| tests/test_application.py | Tests routing, assertions, cache behavior, and PoP safeguards. |
| msal/token_cache.py | Hardens bound-token keys, lookup isolation, and legacy removal. |
| msal/mtls.py | Restricts supported hosts and selects sessions by TLS verification settings. |
| msal/application.py | Implements Bearer-over-mTLS routing and certificate-chain support. |
| docs/index.rst | Documents PoP transport restrictions and rejected-response handling. |
Review effort: Balanced
Reject post overrides centrally for Bearer-over-mTLS grants and capture requests through the mTLS transport in tests. Build ordered PFX issuer chains using signature verification, excluding unrelated and duplicate certificates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Return the binding context for Bearer-over-mTLS results and use it in the public sample and documentation.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This branch has not been deployed
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 Bearer-over-mTLS support, equivalent to MSAL.NET's
CertificateOptions.SendCertificateOverMtls.Set
send_certificate_over_mtls=Trueon an SN/I certificate credential to make MSAL present that certificate during the TLS handshake with the regional or globalmtlsauth.microsoft.comtoken endpoint. MSAL also signs theprivate_key_jwtclient assertion and forces the public certificate chain into itsx5cheader.The resulting access token is an ordinary, non-certificate-bound
Bearertoken:req_cnf,key_id, ortoken_type=mtls_pop.binding_certificate.This remains distinct from
mtls_proof_of_possession=True, which requests a certificate-boundmtls_poptoken, fences its cache entry bykey_id, and returnsbinding_certificate. Per-request mTLS PoP takes precedence when both options are configured.Developer experience
MSAL owns the certificate-bearing token transport for this flow. Custom application HTTP clients and per-request
postoverrides are rejected rather than silently bypassing mTLS.Coverage
mtlsauth.microsoft.comendpoint routing.x5cassertion headers and PEM/PFX certificate-chain ordering.westus3; no downstream mTLS resource call is made because the final token is not certificate-bound.Validation
Targets
rginsburg/sni-mtls-pop.