Skip to content

Add SNI certificate support over mTLS Proof-of-Possession#938

Open
Robbie-Microsoft wants to merge 1 commit into
devfrom
rginsburg/sni-mtls-pop
Open

Add SNI certificate support over mTLS Proof-of-Possession#938
Robbie-Microsoft wants to merge 1 commit into
devfrom
rginsburg/sni-mtls-pop

Conversation

@Robbie-Microsoft

@Robbie-Microsoft Robbie-Microsoft commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds support for using a Subject Name + Issuer (SN/I) certificate as the client credential over mTLS Proof-of-Possession (PoP). A confidential-client app configured with an SN/I certificate can now obtain an mTLS-bound PoP access token from Microsoft Entra (ESTS), where that same certificate is used as the client TLS certificate in the mutual-TLS handshake to the token endpoint.

This closes the gap between the existing SN/I + Bearer flow (cert signs a private_key_jwt assertion) and the new SN/I + mTLS PoP flow (cert is the TLS client cert, ESTS returns token_type=mtls_pop bound via cnf/x5t#S256). The credential is the same; only the mechanism changes (assertion-signer to TLS client cert). Wire behavior mirrors the shipped MSAL.NET implementation.

Note: The optional two-leg Federated Identity Credential (FIC) exchange over mTLS PoP has been split into a stacked follow-up PR: #939.

Public API

Vanilla SN/I -> mTLS PoP:

app = msal.ConfidentialClientApplication(
    client_id,
    authority="https://login.microsoftonline.com/<tenant-id>",  # MUST be tenanted
    client_credential={"private_key_pfx_path": "sni.pfx", "public_certificate": True},
    # azure_region="westus3",  # optional; omit for the global mtls endpoint
)
result = app.acquire_token_for_client(
    ["https://graph.microsoft.com/.default"],
    mtls_proof_of_possession=True,
)
# result["token_type"] == "mtls_pop"
# result["binding_certificate"] == {"x5c": [...], "thumbprint_sha256": "..."}  # public material only

What changed

Core

  • msal/mtls.py (new) -- mTLS client-cert transport: token-endpoint transform, SSLContext built from the cert material, requests adapter injection, plus sovereign-cloud and known-host guardrails.
  • msal/application.py -- mtls_proof_of_possession kwarg on acquire_token_for_client; cert-material plumbing; _MtlsClient; mTLS client selection for cert-bound PoP; binding_certificate in the result; fail-fast guards (tenanted authority, custom http_client, missing cert).
  • msal/token_cache.py -- mtls_pop tokens isolated by key_id (= x5t#S256). Bearer keys are byte-for-byte unchanged; a Bearer request never returns a key-bound token.
  • msal/telemetry.py -- token-type telemetry (mtls_pop -> 6); Bearer unchanged.
  • msal/sku.py -- version bump 1.37.0 -> 1.38.0.

Backward compatibility -- the existing SN/I + Bearer (private_key_jwt / x5c) flow is untouched; the same certificate can be used either way.

Tests

  • tests/test_mtls_transport.py (new, 14) -- endpoint transform, sovereign guardrail, SSLContext build + temp-file cleanup, adapter injection, lazy session.
  • tests/test_application.py (+7) -- vanilla request/result, cache hit, backward-compat Bearer, regional, and validation fail-fasts (secret+flag, custom-http-client, untenanted /common).
  • tests/test_token_cache.py -- Bearer / mtls_pop coexistence / isolation.
  • tests/test_e2e.py (+2) -- E2E-1 vanilla, E2E-2 regional. They run in the existing ADO E2E stage using the non-CNG lab cert, and self-skip when lab creds are absent.

Verification: full unit suite 337 passed, 11 skipped (no regressions); mTLS-focused tests green.

Docs / samples

  • docs/index.rst -- new "mTLS Proof-of-Possession (SN/I certificate)" section (SHR-PoP vs mTLS-PoP, tenanted-authority/region requirements).
  • Docstrings -- acquire_token_for_client mtls_proof_of_possession kwarg + binding_certificate result.
  • sample/confidential_client_mtls_pop_sample.py (new) -- vanilla SN/I -> mTLS PoP example.

Notes / open questions

  • mTLS PoP requires a tenanted authority and MSAL-owned transport (no custom http_client); both are enforced with clear ValueErrors.
  • Currently targets public and Azure Government (Arlington) clouds; other sovereign clouds are fenced off in msal/mtls.py for easy lifting later.
  • The FIC two-leg exchange over mTLS PoP is split out into the stacked follow-up PR Add FIC two-leg exchange over mTLS Proof-of-Possession (follow-up to #938) #939.

Copilot AI review requested due to automatic review settings July 1, 2026 19:41
Comment thread sample/confidential_client_mtls_pop_sample.py Fixed
Comment thread sample/confidential_client_mtls_pop_sample.py Fixed
Comment thread sample/confidential_client_mtls_pop_sample.py Fixed
Comment thread sample/confidential_client_mtls_pop_sample.py Fixed
Comment thread sample/confidential_client_mtls_pop_sample.py Fixed
Comment thread sample/confidential_client_mtls_pop_sample.py Fixed
Comment thread sample/confidential_client_mtls_pop_sample.py Fixed
Comment thread sample/confidential_client_mtls_pop_sample.py Fixed
@Robbie-Microsoft Robbie-Microsoft marked this pull request as ready for review July 1, 2026 19:44
@Robbie-Microsoft Robbie-Microsoft requested a review from a team as a code owner July 1, 2026 19:44

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.

Pull request overview

Adds mTLS Proof-of-Possession support for confidential clients using SN/I certificates, including mTLS-bound token acquisition, transport handling, telemetry updates, and cache isolation so Bearer and key-bound tokens can safely coexist.

Changes:

  • Introduces mTLS transport + endpoint transformation/guardrails for mTLS PoP token requests.
  • Adds mtls_proof_of_possession support to acquire_token_for_client(), including FIC leg-2 over mTLS behavior and public binding material in results.
  • Updates token cache + telemetry to properly isolate and classify mtls_pop tokens; adds unit/E2E tests, docs, and a sample.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/test_token_cache.py Adds coverage ensuring Bearer and mtls_pop ATs coexist and don’t cross-match.
tests/test_optional_thumbprint.py Updates certificate mock shape to include PEM material.
tests/test_mtls_transport.py New tests for host transform/guardrails, SSLContext creation, adapter injection, and lazy session creation.
tests/test_e2e.py Adds E2E scenarios for SN/I over mTLS PoP and FIC two-leg over mTLS.
tests/test_application.py Adds unit tests validating request wiring, cache hits, backward compat, regional routing, FIC leg-2, and guardrails.
sample/confidential_client_mtls_pop_sample.py New sample demonstrating vanilla mTLS PoP and optional FIC two-leg flow.
msal/token_cache.py Adds key_id support to AT cache keys and prevents unkeyed queries from matching keyed entries.
msal/telemetry.py Adds token-type mapping for mtls_pop and emits the platform config field when needed.
msal/sku.py Bumps version to 1.38.0.
msal/oauth2cli/oauth2.py Adds jwt-pop client assertion type constant.
msal/mtls.py New module implementing mtlsauth host mapping/guardrails and a requests transport that presents a client cert.
msal/application.py Adds cert-material plumbing, _MtlsClient, mTLS client selection, cache binding via key_id, and public binding result.
docs/index.rst Documents new mTLS PoP feature, requirements, and usage patterns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread msal/mtls.py
Comment thread msal/token_cache.py Outdated
Comment thread msal/application.py Outdated
Comment thread msal/application.py Outdated
Comment thread msal/application.py
@Robbie-Microsoft Robbie-Microsoft marked this pull request as draft July 1, 2026 20:51
Allow a confidential-client app configured with a Subject Name + Issuer
(SN/I) certificate to obtain an mTLS-bound PoP access token from Entra ID,
using the same certificate as the client TLS certificate in the mutual-TLS
handshake to the token endpoint (token_type=mtls_pop, cnf/x5t#S256 binding).

- Add mtls_proof_of_possession kwarg to acquire_token_for_client, returning
  a binding_certificate (public x5c + sha256 thumbprint) on success
- Add mTLS client-cert transport (msal/mtls.py) with endpoint transform and
  sovereign-cloud / tenanted-authority / custom-http-client guardrails
- Isolate mtls_pop tokens in cache via key_id (Bearer unchanged)
- Add token-type telemetry, docs, and a confidential-client mTLS PoP sample

Co-authored-by: Copilot App <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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread msal/application.py
Comment on lines +1027 to +1031
with self._mtls_lock:
cached = self._mtls_regional_client if region else self._mtls_client
if cached is not None:
return cached
cert = self._get_mtls_pop_cert() # May raise ValueError (no cert configured)
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