Skip to content

Support custom certificate-aware mTLS HTTP pipelines - #974

Open
Robbie-Microsoft wants to merge 7 commits into
rginsburg/sni-mtls-popfrom
robbie-microsoft-mtls-http-extensibility
Open

Robbie-Microsoft wants to merge 7 commits into
rginsburg/sni-mtls-popfrom
robbie-microsoft-mtls-http-extensibility

Conversation

@Robbie-Microsoft

Copy link
Copy Markdown
Contributor

Dependency and scope

Depends on #938. This follow-up targets rginsburg/sni-mtls-pop, starting at 9e35a334cf79cf493409bfa5db53da2b9245690c, rather than dev or either sibling feature branch. It changes mTLS token-endpoint HTTP extensibility, not FIC, Bearer-over-mTLS, managed identity, non-exportable keys, or downstream resource authentication.

Developer experience

The default path still handles certificate-bearing TLS automatically, with no additional configuration. Ordinary non-mTLS custom HTTP clients and per-request post overrides retain their existing behavior.

A custom http_client explicitly opts in by adding create_mtls_client(binding_context) alongside its existing ordinary get/post methods. The argument is the existing public msal.MtlsBindingContext, not a token result. There is no marker class or new application constructor parameter.

For example, inside the custom client's factory:

def create_mtls_client(self, binding_context):
    session = self.new_policy_session()  # Fresh session with existing middleware/policy
    session.mount("https://", binding_context.create_requests_adapter(
        max_retries=self.retries,
        pool_connections=self.pool_connections,
        pool_maxsize=self.pool_maxsize,
        pool_block=True,
    ))
    self.mtls_sessions.append(session)  # Factory owns retention and cleanup
    return session

The new MtlsBindingContext.create_requests_adapter(**HTTPAdapter_options) reuses MSAL's certificate-bearing Requests transport, effective CA selection, proxy handling, and Requests-version compatibility fixes. Other HTTP implementations can use the existing create_ssl_context(verify=...) helper after resolving their effective verification policy.

sample/confidential_client_mtls_http_client_sample.py is a runnable developer example retaining header policy, response middleware, CA/proxy/environment settings, timeout, retry and pooling configuration. It explicitly owns and closes ordinary and derived pipelines. The existing default mTLS sample now points to it.

Isolation, ownership and trust contract

  • MSAL lazily creates one derived token pipeline per application/certificate and reuses it across global/regional token endpoints. Creation is serialized within an application; different applications receive separate factory calls. Rotate certificates by creating a new application/pipeline.
  • Factories must return a fresh client with callable post(url, **kwargs), isolated connection pools and TLS sessions, and the same required networking/middleware policy. The factory owns cleanup, including its own partial constructions; MSAL does not close caller-owned clients.
  • Missing/non-callable capabilities, malformed factory results, returning the ordinary client, and directly reusing a client assigned to another live application fail explicitly. Weak owner tracking does not retain applications or require weak-referenceable custom clients. Factory/network errors propagate without fallback.
  • Both default and custom mTLS token paths reject post/cert overrides and redirect opt-ins before cache lookup. Token transport calls are confined to the configured HTTPS token endpoint with allow_redirects=False; redirect responses, observable redirect history and final-endpoint changes are rejected before caching. Ordinary HTTP behavior is unchanged.
  • This is a trusted implementer contract, not proof that arbitrary code uses the certificate. MSAL cannot inspect nested pools/transports, prove custom verification/proxy policy, detect every reuse after application collection, or undo redirects followed by faulty middleware. The documentation states those responsibilities explicitly.
  • binding_certificate is still returned only for a final mtls_pop result, never for ordinary Bearer. Downgraded responses remain rejected and uncached.

Validation

278 tests passed, with 61 subtests passed, on Windows / Python 3.14.7, Requests 2.34.2, urllib3 2.7.0 and OpenSSL 3.5.7:

python -m pytest -q tests\test_application.py tests\test_mtls_transport.py tests\test_token_cache.py tests\test_mtls_http_client.py tests\test_throttled_http_client.py

Coverage includes malformed factories and propagated errors; legacy/default/ordinary compatibility; concurrent construction; direct-client reuse rejection and application garbage collection; final-token contracts; redirects and request overrides; token/HTTP cache isolation; and factory-owned lifecycle behavior.

Real local TLS evidence: the runnable custom pipeline presents the exact selected peer certificate while ordinary/discovery traffic presents none. Loopback tests retain middleware and timeout policy, verify session/request/environment CA settings and isolated verification pools, reuse connections, rotate between two certificates without shared pools/cache pollution, and traverse an actual local HTTPS CONNECT proxy with the selected certificate. Untrusted-server failures do not fall back to the ordinary client. Existing Requests compatibility regressions also run.

The API documentation passes a clean Sphinx rebuild with warnings treated as errors. Live Entra E2E was not run; local TLS validation uses generated test certificates and requires no lab credentials. No PR is merged by this change.

Allow custom HTTP pipelines to create isolated certificate-bearing token clients using MtlsBindingContext. Preserve networking policy, pooling and Requests TLS compatibility, enforce observable transport contracts, and document ownership and trust boundaries.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Robbie-Microsoft
Robbie-Microsoft requested a review from a team as a code owner October 23, 2026 15:29
@Robbie-Microsoft
Robbie-Microsoft added this pull request to stack #975 October 23, 2026 15:31
os.environ["SCOPE"].split(), mtls_proof_of_possession=True)
print("Acquired mTLS-PoP token:", result.get("token_type") == "mtls_pop")
if "error" in result:
print("Error code:", result["error"])
def _start_server(self, handler, context=None):
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
if context is not None:
server.socket = context.wrap_socket(server.socket, server_side=True)
Preserve custom certificate-scoped pipelines, integrate parent transport and cache fixes, and reconcile redirect test contracts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The endpoint guard raises an exception for otherwise compatible custom responses whose URL metadata is a URL object.

1 open issue
What changed in this PR

Adds certificate-aware custom HTTP pipelines to the existing mTLS token flow while preserving the default transport and ordinary HTTP behavior.

Changes:

  • Adds an opt-in client factory and a public Requests adapter helper.
  • Enforces pipeline isolation, request restrictions, and endpoint checks.
  • Documents ownership requirements and adds a runnable sample with unit and local TLS coverage.
File Description
tests/​test_mtls_http_client.py Tests custom factories, isolation, lifecycle, and local TLS behavior.
tests/​test_application.py Updates redirect tests and transport cleanup.
sample/​confidential_client_mtls_pop_sample.py Updates comments to reference the custom pipeline sample.
sample/​confidential_client_mtls_http_client_sample.py Demonstrates policy-preserving pipelines and explicit cleanup.
msal/​mtls.py Adds adapter customization, ownership tracking, and endpoint guards.
msal/​application.py Integrates lazy custom pipeline creation and request validation.
docs/​index.rst Documents factory usage, trust boundaries, and lifecycle responsibilities.

Review effort: Balanced

Comment thread msal/mtls.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Custom certificate-bearing authentication pipelines introduce security-sensitive isolation, redirect, and lifecycle contracts that warrant final human review.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Robbie-Microsoft and others added 4 commits October 23, 2026 15:09
Distinguish the inherited live matrix from custom HTTP factory loopback TLS coverage without claiming cross-SDK live factory parity.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Allow the mocked legacy release-skip attribute to be created when the E2E module was imported without TRAVIS_TAG.

Co-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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants