Add SNI certificate support over mTLS (Bearer-over-mTLS) - #947
Draft
Robbie-Microsoft wants to merge 6 commits into
Draft
Add SNI certificate support over mTLS (Bearer-over-mTLS)#947Robbie-Microsoft wants to merge 6 commits into
Robbie-Microsoft wants to merge 6 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>
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.
Bearer-over-mTLS (SN/I cert on the handshake -> plain Bearer)
App-level opt-in
client_credential={..., "send_certificate_over_mtls": True}.When set, MSAL presents the SN/I certificate on the TLS handshake to the
[region.]mtlsauth.microsoft.comendpoint and signs a private_key_jwtclient_assertion(x5c forced), but requests a plain Bearer token. Thecertificate authenticates the transport; the token is not bound to it.
Distinct from:
mtls_proof_of_possession=True): binds the token(
token_type=mtls_pop) and fences the cache bykey_id. A per-requestmtls_proof_of_possession=Truealways wins over this app-level flag.(goes to
login.*).Wire contract (flag set AND request not mtls_pop)
https://mtlsauth.microsoft.com/{tenant}/oauth2/v2.0/token,or regional
https://{region}.mtlsauth.microsoft.com/....client_assertion(private_key_jwt) +client_assertion_type=...jwt-bearer, with x5c forced on the assertionheader. NO
req_cnf, NOkey_id, NOtoken_type=mtls_pop.Bearer; the cache entry is plain (not key_id-fenced).Coverage
Ports MSAL.NET
CertificateOptions.SendCertificateOverMtls. This PR lands theclient-credentials leg plus config/builder validation. OBO / refresh /
auth-code + E2E follow.
Targets
rginsburg/sni-mtls-pop(sibling to the FIC PR). Draft / WIP.